diff --git a/src/panels/config/integrations/integration-panels/zha/zha-network-visualization-page.ts b/src/panels/config/integrations/integration-panels/zha/zha-network-visualization-page.ts index 8f3dbd63a5..3369258adb 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-network-visualization-page.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-network-visualization-page.ts @@ -15,6 +15,12 @@ import { fetchDevices, ZHADevice } from "../../../../../data/zha"; import "../../../../../layouts/hass-subpage"; import type { HomeAssistant } from "../../../../../types"; import { Network, Edge, Node, EdgeOptions } from "vis-network"; +import "../../../../../common/search/search-input"; +import "../../../../../components/ha-button-menu"; +import "../../../../../components/device/ha-device-picker"; +import "../../../../../components/ha-svg-icon"; +import { formatAsPaddedHex } from "./functions"; +import { PolymerChangedEvent } from "../../../../../polymer-types"; @customElement("zha-network-visualization-page") export class ZHANetworkVisualizationPage extends LitElement { @@ -28,9 +34,21 @@ export class ZHANetworkVisualizationPage extends LitElement { @internalProperty() private _devices: Map = new Map(); + @internalProperty() + private _devicesByDeviceId: Map = new Map(); + + @internalProperty() + private _nodes: Node[] = []; + @internalProperty() private _network?: Network; + @internalProperty() + private _filter?: string; + + @internalProperty() + private _zoomedDeviceId?: string; + protected firstUpdated(changedProperties: PropertyValues): void { super.firstUpdated(changedProperties); if (this.hass) { @@ -91,6 +109,27 @@ export class ZHANetworkVisualizationPage extends LitElement { "ui.panel.config.zha.visualization.header" )} > +
+ + + +
`; @@ -101,15 +140,18 @@ export class ZHANetworkVisualizationPage extends LitElement { this._devices = new Map( devices.map((device: ZHADevice) => [device.ieee, device]) ); + this._devicesByDeviceId = new Map( + devices.map((device: ZHADevice) => [device.device_reg_id, device]) + ); this._updateDevices(devices); } private _updateDevices(devices: ZHADevice[]) { - const nodes: Node[] = []; + this._nodes = []; const edges: Edge[] = []; devices.forEach((device) => { - nodes.push({ + this._nodes.push({ id: device.ieee, label: this._buildLabel(device), shape: this._getShape(device), @@ -137,7 +179,7 @@ export class ZHANetworkVisualizationPage extends LitElement { } }); - this._network?.setData({ nodes: nodes, edges: edges }); + this._network?.setData({ nodes: this._nodes, edges: edges }); } private _getLQI(lqi: number): EdgeOptions["color"] { @@ -181,7 +223,7 @@ export class ZHANetworkVisualizationPage extends LitElement { label += `IEEE: ${device.ieee}`; label += `\nDevice Type: ${device.device_type.replace("_", " ")}`; if (device.nwk != null) { - label += `\nNWK: ${device.nwk}`; + label += `\nNWK: ${formatAsPaddedHex(device.nwk)}`; } if (device.manufacturer != null && device.model != null) { label += `\nDevice: ${device.manufacturer} ${device.model}`; @@ -194,6 +236,56 @@ export class ZHANetworkVisualizationPage extends LitElement { return label; } + private _handleSearchChange(ev: CustomEvent) { + this._filter = ev.detail.value; + const filterText = this._filter!.toLowerCase(); + if (!this._network) { + return; + } + if (this._filter) { + const filteredNodeIds: (string | number)[] = []; + this._nodes.forEach((node) => { + if (node.label && node.label.toLowerCase().includes(filterText)) { + filteredNodeIds.push(node.id!); + } + }); + this._zoomedDeviceId = ""; + this._zoomOut(); + this._network.selectNodes(filteredNodeIds, true); + } else { + this._network.unselectAll(); + } + } + + private _zoomToDevice(event: PolymerChangedEvent) { + event.stopPropagation(); + this._zoomedDeviceId = event.detail.value; + if (!this._network) { + return; + } + this._filter = ""; + if (!this._zoomedDeviceId) { + this._zoomOut(); + } else { + const device: ZHADevice | undefined = this._devicesByDeviceId.get( + this._zoomedDeviceId + ); + if (device) { + this._network.fit({ + nodes: [device.ieee], + animation: { duration: 500, easingFunction: "easeInQuad" }, + }); + } + } + } + + private _zoomOut() { + this._network!.fit({ + nodes: [], + animation: { duration: 500, easingFunction: "easeOutQuad" }, + }); + } + static get styles(): CSSResult[] { return [ css` @@ -208,6 +300,30 @@ export class ZHANetworkVisualizationPage extends LitElement { line-height: var(--paper-font-display1_-_line-height); opacity: var(--dark-primary-opacity); } + .table-header { + border-bottom: 1px solid --divider-color; + padding: 0 16px; + display: flex; + align-items: center; + height: var(--header-height); + } + .search-toolbar { + display: flex; + align-items: center; + color: var(--secondary-text-color); + padding: 0 16px; + } + search-input { + position: relative; + top: 2px; + flex: 1; + } + search-input.header { + left: -8px; + } + ha-device-picker { + flex: 1; + } `, ]; } diff --git a/src/translations/en.json b/src/translations/en.json index e66b0e5f75..1e9d22dd61 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2271,7 +2271,9 @@ }, "visualization": { "header": "Network Visualization", - "caption": "Visualization" + "caption": "Visualization", + "highlight_label": "Highlight Devices", + "zoom_label": "Zoom To Device" }, "group_binding": { "header": "Group Binding",