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": {