diff --git a/src/data/zha.ts b/src/data/zha.ts index 96c8166a24..f989708257 100644 --- a/src/data/zha.ts +++ b/src/data/zha.ts @@ -89,6 +89,11 @@ export const reconfigureNode = ( ieee: ieeeAddress, }); +export const refreshTopology = (hass: HomeAssistant): Promise => + hass.callWS({ + type: "zha/topology/update", + }); + export const fetchAttributesForCluster = ( hass: HomeAssistant, ieeeAddress: string, diff --git a/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-actions-zha.ts b/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-actions-zha.ts index 40e6c7e79c..b8a179e542 100644 --- a/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-actions-zha.ts +++ b/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-actions-zha.ts @@ -79,6 +79,11 @@ export class HaDeviceActionsZha extends LitElement { "ui.dialogs.zha_device_info.buttons.clusters" )} + + ${this.hass!.localize( + "ui.dialogs.zha_device_info.buttons.view_in_visualization" + )} + ${this.hass!.localize( "ui.dialogs.zha_device_info.buttons.remove" @@ -104,6 +109,13 @@ export class HaDeviceActionsZha extends LitElement { navigate(this, "/config/zha/add/" + this._zhaDevice!.ieee); } + private _onViewInVisualizationClick() { + navigate( + this, + "/config/zha/visualization/" + this._zhaDevice!.device_reg_id + ); + } + private async _handleZigbeeInfoClicked() { showZHADeviceZigbeeInfoDialog(this, { device: this._zhaDevice! }); } diff --git a/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard-router.ts b/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard-router.ts index 5a52b8d5a2..4fcef9343c 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard-router.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard-router.ts @@ -59,6 +59,8 @@ class ZHAConfigDashboardRouter extends HassRouterPage { el.groupId = this.routeTail.path.substr(1); } else if (this._currentPage === "device") { el.ieee = this.routeTail.path.substr(1); + } else if (this._currentPage === "visualization") { + el.zoomedDeviceId = this.routeTail.path.substr(1); } const searchParams = new URLSearchParams(window.location.search); 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 898924a1ab..190c7dd3c2 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 @@ -9,23 +9,33 @@ import { PropertyValues, query, } from "lit-element"; -import { Edge, EdgeOptions, Network, Node } from "vis-network"; + +import "@material/mwc-button"; import { navigate } from "../../../../../common/navigate"; +import { + fetchDevices, + refreshTopology, + 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/device/ha-device-picker"; import "../../../../../components/ha-button-menu"; import "../../../../../components/ha-svg-icon"; -import { fetchDevices, ZHADevice } from "../../../../../data/zha"; -import "../../../../../layouts/hass-subpage"; import { PolymerChangedEvent } from "../../../../../polymer-types"; -import type { HomeAssistant } from "../../../../../types"; import { formatAsPaddedHex } from "./functions"; +import { DeviceRegistryEntry } from "../../../../../data/device_registry"; @customElement("zha-network-visualization-page") export class ZHANetworkVisualizationPage extends LitElement { @property({ type: Object }) public hass!: HomeAssistant; - @property({ type: Boolean }) public narrow!: boolean; + @property({ type: Boolean, reflect: true }) public narrow = false; + + @property() + public zoomedDeviceId?: string; @query("#visualization", true) private _visualization?: HTMLElement; @@ -45,9 +55,6 @@ export class ZHANetworkVisualizationPage extends LitElement { @internalProperty() private _filter?: string; - @internalProperty() - private _zoomedDeviceId?: string; - protected firstUpdated(changedProperties: PropertyValues): void { super.firstUpdated(changedProperties); if (this.hass) { @@ -98,6 +105,12 @@ export class ZHANetworkVisualizationPage extends LitElement { } } }); + + this._network.on("stabilized", () => { + if (this.zoomedDeviceId) { + this._zoomToDevice(); + } + }); } protected render() { @@ -121,13 +134,18 @@ export class ZHANetworkVisualizationPage extends LitElement { this._filterDevices(device)} + @value-changed=${this._onZoomToDevice} > + ${this.hass!.localize( + "ui.panel.config.zha.visualization.refresh_topology" + )}
@@ -248,7 +266,7 @@ export class ZHANetworkVisualizationPage extends LitElement { filteredNodeIds.push(node.id!); } }); - this._zoomedDeviceId = ""; + this.zoomedDeviceId = ""; this._zoomOut(); this._network.selectNodes(filteredNodeIds, true); } else { @@ -256,21 +274,25 @@ export class ZHANetworkVisualizationPage extends LitElement { } } - private _zoomToDevice(event: PolymerChangedEvent) { + private _onZoomToDevice(event: PolymerChangedEvent) { event.stopPropagation(); - this._zoomedDeviceId = event.detail.value; + this.zoomedDeviceId = event.detail.value; if (!this._network) { return; } + this._zoomToDevice(); + } + + private _zoomToDevice() { this._filter = ""; - if (!this._zoomedDeviceId) { + if (!this.zoomedDeviceId) { this._zoomOut(); } else { const device: ZHADevice | undefined = this._devicesByDeviceId.get( - this._zoomedDeviceId + this.zoomedDeviceId ); if (device) { - this._network.fit({ + this._network!.fit({ nodes: [device.ieee], animation: { duration: 500, easingFunction: "easeInQuad" }, }); @@ -285,6 +307,24 @@ export class ZHANetworkVisualizationPage extends LitElement { }); } + private async _refreshTopology(): Promise { + await refreshTopology(this.hass); + } + + private _filterDevices(device: DeviceRegistryEntry): boolean { + if (!this.hass) { + return false; + } + for (const parts of device.identifiers) { + for (const part of parts) { + if (part === "zha") { + return true; + } + } + } + return false; + } + static get styles(): CSSResult[] { return [ css` @@ -299,30 +339,59 @@ 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; + flex-direction: row; height: var(--header-height); } + + :host([narrow]) .table-header { + flex-direction: column; + align-items: stretch; + height: var(--header-height) * 3; + } + .search-toolbar { display: flex; align-items: center; color: var(--secondary-text-color); padding: 0 16px; } + search-input { position: relative; top: 2px; flex: 1; } + + :host(:not([narrow])) search-input { + margin: 5px; + } + search-input.header { left: -8px; } + ha-device-picker { flex: 1; } + + :host(:not([narrow])) ha-device-picker { + margin: 5px; + } + + mwc-button { + font-weight: 500; + color: var(--primary-color); + } + + :host(:not([narrow])) mwc-button { + margin: 5px; + } `, ]; } diff --git a/src/translations/en.json b/src/translations/en.json index 0e7abc790f..11c387e511 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -745,7 +745,8 @@ "remove": "Remove Device", "clusters": "Manage Clusters", "reconfigure": "Reconfigure Device", - "zigbee_information": "Zigbee device signature" + "zigbee_information": "Zigbee device signature", + "view_in_visualization": "View in Visualization" }, "services": { "reconfigure": "Reconfigure ZHA device (heal device). Use this if you are having issues with the device. If the device in question is a battery powered device please ensure it is awake and accepting commands when you use this service.", @@ -2365,7 +2366,8 @@ "header": "Network Visualization", "caption": "Visualization", "highlight_label": "Highlight Devices", - "zoom_label": "Zoom To Device" + "zoom_label": "Zoom To Device", + "refresh_topology": "Refresh Topology" }, "group_binding": { "header": "Group Binding",