From 45df2d977c7fe1dc524fbced481f42a8c1766d24 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 8 Jul 2025 15:55:54 +0300 Subject: [PATCH] working version --- src/components/chart/ha-network-graph.ts | 2 - .../zha/zha-network-visualization-page.ts | 1 + .../zwave_js-network-visualization.ts | 165 +++++++++++++++--- src/translations/en.json | 23 ++- 4 files changed, 163 insertions(+), 28 deletions(-) diff --git a/src/components/chart/ha-network-graph.ts b/src/components/chart/ha-network-graph.ts index aa4052afcf..62c2d8b7d4 100644 --- a/src/components/chart/ha-network-graph.ts +++ b/src/components/chart/ha-network-graph.ts @@ -210,10 +210,8 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) { // set the position of the node at polarDistance from the center in a random direction const angle = Math.random() * 2 * Math.PI; echartsNode.x = - containerWidth / 2 + ((Math.cos(angle) * containerWidth) / 2) * node.polarDistance; echartsNode.y = - containerHeight / 2 + ((Math.sin(angle) * containerHeight) / 2) * node.polarDistance; this._nodePositions[node.id] = { x: echartsNode.x, 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 d6eefa08b9..dffe1d8c7f 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 @@ -226,6 +226,7 @@ export class ZHANetworkVisualizationPage extends LitElement { : offlineColor, }, polarDistance: category === 0 ? 0 : category === 1 ? 0.5 : 0.9, + fixed: isCoordinator, }); // Create links (edges) 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 index 8beee0ef38..672c7d6812 100644 --- 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 @@ -1,6 +1,11 @@ import { customElement, property, state } from "lit/decorators"; import { css, html, LitElement } from "lit"; import memoizeOne from "memoize-one"; +import { mdiUpdate } from "@mdi/js"; +import type { + CallbackDataParams, + TopLevelFormatterParams, +} from "echarts/types/dist/shared"; import type { HomeAssistant, Route } from "../../../../../types"; import { configTabs } from "./zwave_js-config-router"; import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin"; @@ -23,6 +28,7 @@ import type { import { colorVariables } from "../../../../../resources/theme/color.globals"; import type { DeviceRegistryEntry } from "../../../../../data/device_registry"; import { debounce } from "../../../../../common/util/debounce"; +import { navigate } from "../../../../../common/navigate"; @customElement("zwave_js-network-visualization") export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) { @@ -45,6 +51,8 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) { @state() private _devices: Record = {}; + @state() private _live = false; + public hassSubscribe() { const devices = Object.values(this.hass.devices).filter((device) => device.config_entries.some((entry) => entry === this.configEntryId) @@ -54,8 +62,11 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) { subscribeZwaveNodeStatistics(this.hass!, device.id, (message) => { const nodeId = message.nodeId ?? message.node_id; this._devices[nodeId!] = device; + const isNew = !this._nodeStatistics[nodeId!]; this._nodeStatistics[nodeId!] = message; - this._handleUpdatedNodeStatistics(); + if (this._live || isNew) { + this._handleUpdatedNodeStatistics(); + } }) ); } @@ -79,8 +90,18 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) { this._nodeStatuses, this._nodeStatistics )} + .tooltipFormatter=${this._tooltipFormatter} @chart-click=${this._handleChartClick} - > + `; } @@ -100,6 +121,45 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) { // console.log("neighbors", neighbors); } + private _tooltipFormatter = (params: TopLevelFormatterParams): string => { + const { dataType, data } = params as CallbackDataParams; + if (dataType === "edge") { + const { source, target } = data as any; + const sourceDevice = this._devices[source]; + const targetDevice = this._devices[target]; + const sourceName = + sourceDevice?.name_by_user ?? sourceDevice?.name ?? source; + const targetName = + targetDevice?.name_by_user ?? targetDevice?.name ?? target; + let tip = `${sourceName} → ${targetName}`; + const route = + this._nodeStatistics[source]?.lwr || this._nodeStatistics[source]?.nlwr; + if (route?.protocol_data_rate) { + tip += `
${this.hass.localize("ui.panel.config.zwave_js.visualization.data_rate")}: ${this.hass.localize(`ui.panel.config.zwave_js.protocol_data_rate.${route.protocol_data_rate}`)}`; + } + if (route?.rssi) { + tip += `
RSSI: ${route.rssi}`; + } + return tip; + } + const { id, name } = data as any; + const device = this._devices[id]; + const nodeStatus = this._nodeStatuses[id]; + let tip = `${(params as any).marker} ${name}`; + tip += `
${this.hass.localize("ui.panel.config.zwave_js.visualization.node_id")}: ${id}`; + if (device) { + tip += `
${this.hass.localize("ui.panel.config.zwave_js.visualization.manufacturer")}: ${device.manufacturer || "-"}`; + tip += `
${this.hass.localize("ui.panel.config.zwave_js.visualization.model")}: ${device.model || "-"}`; + } + if (nodeStatus) { + tip += `
${this.hass.localize("ui.panel.config.zwave_js.visualization.status")}: ${this.hass.localize(`ui.panel.config.zwave_js.node_status.${nodeStatus.status}`)}`; + if (nodeStatus.zwave_plus_version) { + tip += `
Z-Wave Plus: ${this.hass.localize("ui.panel.config.zwave_js.visualization.version")} ${nodeStatus.zwave_plus_version}`; + } + } + return tip; + }; + private _getNetworkData = memoizeOne( ( nodeStatuses: Record, @@ -158,12 +218,7 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) { nodes.push({ id: String(node.node_id), name: device?.name_by_user ?? device?.name ?? String(node.node_id), - fixed: node.is_controller_node, - polarDistance: node.is_controller_node - ? 0 - : node.status === NodeStatus.Dead - ? 1 - : 0.5, + value: node.is_controller_node ? 3 : node.is_routing ? 2 : 1, category: node.status === NodeStatus.Dead ? 3 @@ -172,6 +227,7 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) { : node.is_controller_node ? 0 : 1, + symbolSize: node.is_controller_node ? 40 : node.is_routing ? 30 : 20, symbol: node.is_controller_node ? "roundRect" : "circle", itemStyle: { color: @@ -183,6 +239,12 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) { ? colorVariables["primary-color"] : colorVariables["cyan-color"], }, + polarDistance: node.is_controller_node + ? 0 + : node.status === NodeStatus.Dead + ? 0.9 + : 0.5, + fixed: node.is_controller_node, }); }); @@ -190,22 +252,52 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) { const route = stats.lwr || stats.nlwr; if (route) { const hops = [ - ...route.repeaters - .map( - (id) => - Object.entries(this._devices).find( - ([_nodeId, d]) => d.id === id - )?.[0] - ) - .filter(Boolean), - controllerNode!, + ...route.repeaters.map((id, i) => [ + Object.keys(this._devices).find( + (_nodeId) => this._devices[_nodeId]?.id === id + )?.[0], + route.repeater_rssi[i], + ]), + [controllerNode!, route.rssi], ]; - let sourceNode = nodeId; - hops.forEach((repeater) => { - links.push({ - source: String(sourceNode), - target: String(repeater), - }); + let sourceNode: string = nodeId; + hops.forEach(([repeater, rssi]) => { + const RSSI = typeof rssi === "number" && rssi <= 0 ? rssi : -100; + const existingLink = links.find( + (link) => + link.source === sourceNode && link.target === String(repeater) + ); + const width = this._getLineWidth(RSSI); + if (existingLink) { + existingLink.value = Math.max(existingLink.value!, RSSI); + existingLink.lineStyle = { + ...existingLink.lineStyle, + width: Math.max(existingLink.lineStyle!.width!, width), + color: + route.protocol_data_rate && RSSI > -100 + ? colorVariables["primary-color"] + : existingLink.lineStyle!.color, + type: + route.protocol_data_rate > 1 + ? "solid" + : existingLink.lineStyle!.type, + }; + } else { + links.push({ + source: sourceNode, + target: String(repeater), + value: RSSI, + lineStyle: { + width, + color: + route.protocol_data_rate && RSSI > -100 + ? colorVariables["primary-color"] + : colorVariables["disabled-color"], + type: route.protocol_data_rate > 1 ? "solid" : "dotted", + }, + symbolSize: width * 3, + }); + } sourceNode = String(repeater); }); } @@ -221,8 +313,31 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) { this._nodeStatistics = { ...this._nodeStatistics }; }, 500); - private _handleChartClick(_e: CustomEvent) { - // @TODO + private _handleChartClick(e: CustomEvent) { + if ( + e.detail.dataType === "node" && + e.detail.event.target.cursor === "pointer" + ) { + const { id } = e.detail.data; + const device = this._devices[id]; + if (device) { + navigate(`/config/devices/device/${device.id}`); + } + } + } + + private _toggleLive() { + this._live = !this._live; + if (this._live) { + this._fetchNetworkStatus(); + this._handleUpdatedNodeStatistics(); + } else { + this._handleUpdatedNodeStatistics.cancel(); + } + } + + private _getLineWidth(rssi: number): number { + return rssi > -33 ? 3 : rssi > -66 ? 2 : 1; } static get styles() { diff --git a/src/translations/en.json b/src/translations/en.json index 131d904476..a045e3fa87 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -6319,7 +6319,28 @@ "controller": "Controller", "node": "Node", "asleep_node": "Asleep Node", - "dead_node": "Dead Node" + "dead_node": "Dead Node", + "toggle_live": "Toggle live updates", + "node_id": "Node ID", + "manufacturer": "Manufacturer", + "model": "Model", + "status": "Status", + "version": "Version", + "data_rate": "Data rate" + }, + "node_status": { + "0": "Unknown", + "1": "Asleep", + "2": "Awake", + "3": "Dead", + "4": "Alive" + }, + "protocol_data_rate": { + "0": "Unspecified", + "1": "Z-Wave 9.6 kbps", + "2": "Z-Wave 40 kbps", + "3": "Z-Wave 100 kbps", + "4": "Long Range 100 kbps" }, "node_installer": { "header": "Installer settings",