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 b8a179e542..ddca57c922 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 @@ -21,6 +21,7 @@ import { haStyle } from "../../../../../../resources/styles"; import { HomeAssistant } from "../../../../../../types"; import { showZHAClusterDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-cluster"; import { showZHADeviceZigbeeInfoDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info"; +import { showZHADeviceChildrenDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-children"; @customElement("ha-device-actions-zha") export class HaDeviceActionsZha extends LitElement { @@ -65,6 +66,11 @@ export class HaDeviceActionsZha extends LitElement { ${this.hass!.localize("ui.dialogs.zha_device_info.buttons.add")} + + ${this.hass!.localize( + "ui.dialogs.zha_device_info.buttons.device_children" + )} + ` : ""} ${this._zhaDevice.device_type !== "Coordinator" @@ -120,6 +126,10 @@ export class HaDeviceActionsZha extends LitElement { showZHADeviceZigbeeInfoDialog(this, { device: this._zhaDevice! }); } + private async _handleDeviceChildrenClicked() { + showZHADeviceChildrenDialog(this, { device: this._zhaDevice! }); + } + private async _removeDevice() { const confirmed = await showConfirmationDialog(this, { text: this.hass.localize( diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-children.ts b/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-children.ts new file mode 100644 index 0000000000..31869775cc --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-children.ts @@ -0,0 +1,146 @@ +import { + CSSResult, + customElement, + html, + internalProperty, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import memoizeOne from "memoize-one"; +import { computeRTLDirection } from "../../../../../common/util/compute_rtl"; +import "../../../../../components/ha-code-editor"; +import { createCloseHeading } from "../../../../../components/ha-dialog"; +import { haStyleDialog } from "../../../../../resources/styles"; +import { HomeAssistant } from "../../../../../types"; +import { ZHADeviceChildrenDialogParams } from "./show-dialog-zha-device-children"; +import "../../../../../components/data-table/ha-data-table"; +import type { + DataTableColumnContainer, + DataTableRowData, +} from "../../../../../components/data-table/ha-data-table"; +import "../../../../../components/ha-circular-progress"; +import { fetchDevices, ZHADevice } from "../../../../../data/zha"; +import { fireEvent } from "../../../../../common/dom/fire_event"; + +export interface DeviceRowData extends DataTableRowData { + id: string; + name: string; + lqi: number; +} + +@customElement("dialog-zha-device-children") +class DialogZHADeviceChildren extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @internalProperty() private _device: ZHADevice | undefined; + + @internalProperty() private _devices: Map | undefined; + + private _deviceChildren = memoizeOne( + ( + device: ZHADevice | undefined, + devices: Map | undefined + ) => { + const outputDevices: DeviceRowData[] = []; + if (device && devices) { + device.neighbors.forEach((child) => { + const zhaDevice: ZHADevice | undefined = devices.get(child.ieee); + if (zhaDevice) { + outputDevices.push({ + name: zhaDevice.user_given_name || zhaDevice.name, + id: zhaDevice.device_reg_id, + lqi: child.lqi, + }); + } + }); + } + return outputDevices; + } + ); + + private _columns: DataTableColumnContainer = { + name: { + title: "Name", + sortable: true, + filterable: true, + direction: "asc", + grows: true, + }, + lqi: { + title: "LQI", + sortable: true, + filterable: true, + direction: "asc", + width: "75px", + }, + }; + + public showDialog( + params: ZHADeviceChildrenDialogParams + ): void { + this._device = params.device; + this._fetchData(); + } + + public closeDialog(): void { + this._device = undefined; + this._devices = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._device) { + return html``; + } + return html` + + ${!this._devices + ? html`` + : html``} + + `; + } + + private async _fetchData(): Promise { + if (this._device && this.hass) { + const devices = await fetchDevices(this.hass!); + this._devices = new Map( + devices.map((device: ZHADevice) => [device.ieee, device]) + ); + } + } + + static get styles(): CSSResult { + return haStyleDialog; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-zha-device-children": DialogZHADeviceChildren; + } +} diff --git a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-children.ts b/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-children.ts new file mode 100644 index 0000000000..f8a3e64ab0 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-children.ts @@ -0,0 +1,20 @@ +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { ZHADevice } from "../../../../../data/zha"; + +export interface ZHADeviceChildrenDialogParams { + device: ZHADevice; +} + +export const loadZHADeviceChildrenDialog = () => + import("./dialog-zha-device-children"); + +export const showZHADeviceChildrenDialog = ( + element: HTMLElement, + zhaDeviceChildrenParams: ZHADeviceChildrenDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-zha-device-children", + dialogImport: loadZHADeviceChildrenDialog, + dialogParams: zhaDeviceChildrenParams, + }); +}; 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 190c7dd3c2..6cbe4e77be 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 @@ -27,6 +27,8 @@ import "../../../../../components/ha-svg-icon"; import { PolymerChangedEvent } from "../../../../../polymer-types"; import { formatAsPaddedHex } from "./functions"; import { DeviceRegistryEntry } from "../../../../../data/device_registry"; +import "../../../../../components/ha-checkbox"; +import type { HaCheckbox } from "../../../../../components/ha-checkbox"; @customElement("zha-network-visualization-page") export class ZHANetworkVisualizationPage extends LitElement { @@ -55,11 +57,15 @@ export class ZHANetworkVisualizationPage extends LitElement { @internalProperty() private _filter?: string; + private _autoZoom = true; + protected firstUpdated(changedProperties: PropertyValues): void { super.firstUpdated(changedProperties); + if (this.hass) { this._fetchData(); } + this._network = new Network( this._visualization!, {}, @@ -92,6 +98,7 @@ export class ZHANetworkVisualizationPage extends LitElement { }, } ); + this._network.on("doubleClick", (properties) => { const ieee = properties.nodes[0]; if (ieee) { @@ -106,6 +113,17 @@ export class ZHANetworkVisualizationPage extends LitElement { } }); + this._network.on("click", (properties) => { + const ieee = properties.nodes[0]; + if (ieee) { + const device = this._devices.get(ieee); + if (device && this._autoZoom) { + this.zoomedDeviceId = device.device_reg_id; + this._zoomToDevice(); + } + } + }); + this._network.on("stabilized", () => { if (this.zoomedDeviceId) { this._zoomToDevice(); @@ -141,6 +159,11 @@ export class ZHANetworkVisualizationPage extends LitElement { .deviceFilter=${(device) => this._filterDevices(device)} @value-changed=${this._onZoomToDevice} > + ${this.hass!.localize("ui.panel.config.zha.visualization.auto_zoom")} ${this.hass!.localize( "ui.panel.config.zha.visualization.refresh_topology" @@ -325,6 +348,10 @@ export class ZHANetworkVisualizationPage extends LitElement { return false; } + private _handleCheckboxChange(ev: Event) { + this._autoZoom = (ev.target as HaCheckbox).checked; + } + static get styles(): CSSResult[] { return [ css` diff --git a/src/translations/en.json b/src/translations/en.json index d8d640e3bb..ddfaf478a3 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -743,12 +743,14 @@ "manuf": "by {manufacturer}", "no_area": "No Area", "device_signature": "Zigbee device signature", + "device_children": "Zigbee device children", "buttons": { "add": "Add Devices via this device", "remove": "Remove Device", "clusters": "Manage Clusters", "reconfigure": "Reconfigure Device", "zigbee_information": "Zigbee device signature", + "device_children": "View Children", "view_in_visualization": "View in Visualization" }, "services": { @@ -2373,6 +2375,7 @@ "caption": "Visualization", "highlight_label": "Highlight Devices", "zoom_label": "Zoom To Device", + "auto_zoom": "Auto Zoom", "refresh_topology": "Refresh Topology" }, "group_binding": {