From 100525735c5a34f93144a4de7ea4258d7adfeeda Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Wed, 18 Jun 2025 15:00:21 +0300 Subject: [PATCH] ZwaveJS network visualization --- .../zwave_js/zwave_js-config-router.ts | 11 +- .../zwave_js-network-visualization.ts | 111 ++++++++++++++++++ src/translations/en.json | 7 +- 3 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 src/panels/config/integrations/integration-panels/zwave_js/zwave_js-network-visualization.ts diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-router.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-router.ts index 83cac24c77..25cffcf1db 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-router.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-router.ts @@ -1,4 +1,4 @@ -import { mdiServerNetwork, mdiMathLog } from "@mdi/js"; +import { mdiServerNetwork, mdiMathLog, mdiNetwork } from "@mdi/js"; import { customElement, property } from "lit/decorators"; import type { RouterOptions } from "../../../../../layouts/hass-router-page"; import { HassRouterPage } from "../../../../../layouts/hass-router-page"; @@ -18,6 +18,11 @@ export const configTabs: PageNavigation[] = [ path: `/config/zwave_js/logs`, iconPath: mdiMathLog, }, + { + translationKey: "ui.panel.config.zwave_js.navigation.visualization", + path: `/config/zwave_js/visualization`, + iconPath: mdiNetwork, + }, ]; @customElement("zwave_js-config-router") @@ -60,6 +65,10 @@ class ZWaveJSConfigRouter extends HassRouterPage { tag: "zwave_js-provisioned", load: () => import("./zwave_js-provisioned"), }, + visualization: { + tag: "zwave_js-network-visualization", + load: () => import("./zwave_js-network-visualization"), + }, }, initialLoad: () => this._fetchConfigEntries(), }; diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-network-visualization.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-network-visualization.ts new file mode 100644 index 0000000000..d6b8d5a68c --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-network-visualization.ts @@ -0,0 +1,111 @@ +import { customElement, property, state } from "lit/decorators"; +import { css, html, LitElement, nothing } from "lit"; +import memoizeOne from "memoize-one"; +import type { HomeAssistant, Route } from "../../../../../types"; +import { configTabs } from "./zwave_js-config-router"; +import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin"; +import type { + NetworkData, + NetworkLink, + NetworkNode, +} from "../../../../../components/chart/ha-network-graph"; +import "../../../../../components/chart/ha-network-graph"; +import "../../../../../layouts/hass-tabs-subpage"; +import { fetchZwaveNetworkStatus } from "../../../../../data/zwave_js"; +import { colorVariables } from "../../../../../resources/theme/color.globals"; + +@customElement("zwave_js-network-visualization") +export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) { + public hass!: HomeAssistant; + + @property({ attribute: false }) public route!: Route; + + @property({ attribute: "is-wide", type: Boolean }) public isWide = false; + + @property({ type: Boolean }) public narrow = false; + + @property({ attribute: false }) public configEntryId!: string; + + @state() private _data: NetworkData | null = null; + + protected async firstUpdated() { + this._data = await this._getNetworkData(); + } + + protected render() { + if (!this._data) { + return nothing; + } + return html` + + + `; + } + + private _getNetworkData = memoizeOne(async (): Promise => { + const nodes: NetworkNode[] = []; + const links: NetworkLink[] = []; + const categories = [ + { + name: this.hass.localize( + "ui.panel.config.zwave_js.visualization.controller" + ), + symbol: "roundRect", + itemStyle: { + color: colorVariables["primary-color"], + }, + }, + { + name: this.hass.localize("ui.panel.config.zwave_js.visualization.node"), + symbol: "circle", + itemStyle: { + color: colorVariables["cyan-color"], + }, + }, + ]; + const network = await fetchZwaveNetworkStatus(this.hass!, { + entry_id: this.configEntryId, + }); + network.controller.nodes.forEach((node) => { + nodes.push({ + id: String(node.node_id), + label: String(node.node_id), + fixed: node.is_controller_node, + polarDistance: node.is_controller_node ? 0 : 0.5, + category: node.is_controller_node ? 0 : 1, + }); + }); + + return { nodes, links, categories }; + }); + + private _handleChartClick(_e: CustomEvent) { + // @TODO + } + + static get styles() { + return [ + css` + ha-network-graph { + height: 100%; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "zwave_js-network-visualization": ZWaveJSNetworkVisualization; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index b352f0a58e..915d0fac6a 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5792,7 +5792,8 @@ "zwave_js": { "navigation": { "network": "Network", - "logs": "Logs" + "logs": "Logs", + "visualization": "Visualization" }, "common": { "network": "Network", @@ -6246,6 +6247,10 @@ "log_level_changed": "Log Level changed to: {level}", "download_logs": "Download logs" }, + "visualization": { + "controller": "Controller", + "node": "Node" + }, "node_installer": { "header": "Installer Settings", "introduction": "Configure your device installer settings.",