mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 01:36:49 +00:00
ZWaveJS network graph (#26112)
* ZwaveJS network visualization * more progress * working version * lint * remove unused code * Update src/translations/en.json Co-authored-by: Norbert Rittel <norbert@rittel.de> * remove "live" toggle and use deepEqual * styling tweak --------- Co-authored-by: Norbert Rittel <norbert@rittel.de>
This commit is contained in:
parent
f6aa55ef74
commit
faae7a2322
@ -10,12 +10,13 @@ import type { ECOption } from "../../resources/echarts";
|
||||
import "./ha-chart-base";
|
||||
import type { HaChartBase } from "./ha-chart-base";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
|
||||
export interface NetworkNode {
|
||||
id: string;
|
||||
name?: string;
|
||||
category?: number;
|
||||
label?: string;
|
||||
value?: number;
|
||||
symbolSize?: number;
|
||||
symbol?: string;
|
||||
@ -60,7 +61,7 @@ export interface NetworkData {
|
||||
let GraphChart: typeof import("echarts/lib/chart/graph/install");
|
||||
|
||||
@customElement("ha-network-graph")
|
||||
export class HaNetworkGraph extends LitElement {
|
||||
export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
public chart?: EChartsType;
|
||||
|
||||
@property({ attribute: false }) public data!: NetworkData;
|
||||
@ -77,8 +78,6 @@ export class HaNetworkGraph extends LitElement {
|
||||
|
||||
@state() private _showLabels = true;
|
||||
|
||||
private _listeners: (() => void)[] = [];
|
||||
|
||||
private _nodePositions: Record<string, { x: number; y: number }> = {};
|
||||
|
||||
@query("ha-chart-base") private _baseChart?: HaChartBase;
|
||||
@ -93,22 +92,14 @@ export class HaNetworkGraph extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._listeners.push(
|
||||
protected hassSubscribe() {
|
||||
return [
|
||||
listenMediaQuery("(prefers-reduced-motion)", (matches) => {
|
||||
if (this._reducedMotion !== matches) {
|
||||
this._reducedMotion = matches;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
while (this._listeners.length) {
|
||||
this._listeners.pop()!();
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@ -122,7 +113,7 @@ export class HaNetworkGraph extends LitElement {
|
||||
this._physicsEnabled,
|
||||
this._reducedMotion,
|
||||
this._showLabels
|
||||
)}
|
||||
) as GraphSeriesOption}
|
||||
.options=${this._createOptions(this.data?.categories)}
|
||||
height="100%"
|
||||
.extraComponents=${[GraphChart]}
|
||||
@ -168,7 +159,8 @@ export class HaNetworkGraph extends LitElement {
|
||||
type: "inside",
|
||||
filterMode: "none",
|
||||
},
|
||||
})
|
||||
}),
|
||||
deepEqual
|
||||
);
|
||||
|
||||
private _getSeries = memoizeOne(
|
||||
@ -180,75 +172,80 @@ export class HaNetworkGraph extends LitElement {
|
||||
) => {
|
||||
const containerWidth = this.clientWidth;
|
||||
const containerHeight = this.clientHeight;
|
||||
return [
|
||||
{
|
||||
id: "network",
|
||||
type: "graph",
|
||||
layout: physicsEnabled ? "force" : "none",
|
||||
draggable: true,
|
||||
roam: true,
|
||||
selectedMode: "single",
|
||||
label: {
|
||||
show: showLabels,
|
||||
position: "right",
|
||||
},
|
||||
emphasis: {
|
||||
focus: "adjacency",
|
||||
},
|
||||
force: {
|
||||
repulsion: [400, 600],
|
||||
edgeLength: [200, 300],
|
||||
gravity: 0.1,
|
||||
layoutAnimation: !reducedMotion && data.nodes.length < 100,
|
||||
},
|
||||
edgeSymbol: ["none", "arrow"],
|
||||
edgeSymbolSize: 10,
|
||||
data: data.nodes.map((node) => {
|
||||
const echartsNode: NonNullable<GraphSeriesOption["data"]>[number] =
|
||||
{
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
category: node.category,
|
||||
value: node.value,
|
||||
symbolSize: node.symbolSize || 30,
|
||||
symbol: node.symbol || "circle",
|
||||
itemStyle: node.itemStyle || {},
|
||||
fixed: node.fixed,
|
||||
};
|
||||
if (this._nodePositions[node.id]) {
|
||||
echartsNode.x = this._nodePositions[node.id].x;
|
||||
echartsNode.y = this._nodePositions[node.id].y;
|
||||
} else if (typeof node.polarDistance === "number") {
|
||||
// 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,
|
||||
y: echartsNode.y,
|
||||
};
|
||||
}
|
||||
return echartsNode;
|
||||
}),
|
||||
links: data.links.map((link) => ({
|
||||
...link,
|
||||
value: link.reverseValue
|
||||
? Math.max(link.value ?? 0, link.reverseValue)
|
||||
: link.value,
|
||||
// remove arrow for bidirectional links
|
||||
symbolSize: link.reverseValue ? 1 : link.symbolSize, // 0 doesn't work
|
||||
})),
|
||||
categories: data.categories || [],
|
||||
return {
|
||||
id: "network",
|
||||
type: "graph",
|
||||
layout: physicsEnabled ? "force" : "none",
|
||||
draggable: true,
|
||||
roam: true,
|
||||
selectedMode: "single",
|
||||
label: {
|
||||
show: showLabels,
|
||||
position: "right",
|
||||
},
|
||||
] as any;
|
||||
}
|
||||
emphasis: {
|
||||
focus: "adjacency",
|
||||
},
|
||||
force: {
|
||||
repulsion: [400, 600],
|
||||
edgeLength: [200, 300],
|
||||
gravity: 0.1,
|
||||
layoutAnimation: !reducedMotion && data.nodes.length < 100,
|
||||
},
|
||||
edgeSymbol: ["none", "arrow"],
|
||||
edgeSymbolSize: 10,
|
||||
data: data.nodes.map((node) => {
|
||||
const echartsNode: NonNullable<GraphSeriesOption["data"]>[number] = {
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
category: node.category,
|
||||
value: node.value,
|
||||
symbolSize: node.symbolSize || 30,
|
||||
symbol: node.symbol || "circle",
|
||||
itemStyle: node.itemStyle || {},
|
||||
fixed: node.fixed,
|
||||
};
|
||||
if (this._nodePositions[node.id]) {
|
||||
echartsNode.x = this._nodePositions[node.id].x;
|
||||
echartsNode.y = this._nodePositions[node.id].y;
|
||||
} else if (typeof node.polarDistance === "number") {
|
||||
// set the position of the node at polarDistance from the center in a random direction
|
||||
const angle = Math.random() * 2 * Math.PI;
|
||||
echartsNode.x =
|
||||
((Math.cos(angle) * containerWidth) / 2) * node.polarDistance;
|
||||
echartsNode.y =
|
||||
((Math.sin(angle) * containerHeight) / 2) * node.polarDistance;
|
||||
this._nodePositions[node.id] = {
|
||||
x: echartsNode.x,
|
||||
y: echartsNode.y,
|
||||
};
|
||||
}
|
||||
return echartsNode;
|
||||
}),
|
||||
links: data.links.map((link) => ({
|
||||
...link,
|
||||
value: link.reverseValue
|
||||
? Math.max(link.value ?? 0, link.reverseValue)
|
||||
: link.value,
|
||||
// remove arrow for bidirectional links
|
||||
symbolSize: link.reverseValue ? 1 : link.symbolSize, // 0 doesn't work
|
||||
})),
|
||||
categories: data.categories || [],
|
||||
};
|
||||
},
|
||||
deepEqual
|
||||
);
|
||||
|
||||
private _togglePhysics() {
|
||||
this._saveNodePositions();
|
||||
this._physicsEnabled = !this._physicsEnabled;
|
||||
}
|
||||
|
||||
private _toggleLabels() {
|
||||
this._showLabels = !this._showLabels;
|
||||
}
|
||||
|
||||
private _saveNodePositions() {
|
||||
if (this._baseChart?.chart) {
|
||||
this._baseChart.chart
|
||||
// @ts-ignore private method but no other way to get the graph positions
|
||||
@ -265,11 +262,6 @@ export class HaNetworkGraph extends LitElement {
|
||||
}
|
||||
});
|
||||
}
|
||||
this._physicsEnabled = !this._physicsEnabled;
|
||||
}
|
||||
|
||||
private _toggleLabels() {
|
||||
this._showLabels = !this._showLabels;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
|
@ -358,6 +358,8 @@ export enum ProtocolDataRate {
|
||||
export interface ZWaveJSNodeStatisticsUpdatedMessage {
|
||||
event: "statistics updated";
|
||||
source: "node";
|
||||
nodeId?: number;
|
||||
node_id?: number;
|
||||
commands_tx: number;
|
||||
commands_rx: number;
|
||||
commands_dropped_tx: number;
|
||||
|
@ -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)
|
||||
|
@ -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(),
|
||||
};
|
||||
|
@ -0,0 +1,326 @@
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import memoizeOne from "memoize-one";
|
||||
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";
|
||||
import type {
|
||||
NetworkData,
|
||||
NetworkLink,
|
||||
NetworkNode,
|
||||
} from "../../../../../components/chart/ha-network-graph";
|
||||
import "../../../../../components/chart/ha-network-graph";
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import {
|
||||
fetchZwaveNetworkStatus,
|
||||
NodeStatus,
|
||||
subscribeZwaveNodeStatistics,
|
||||
} from "../../../../../data/zwave_js";
|
||||
import type {
|
||||
ZWaveJSNodeStatisticsUpdatedMessage,
|
||||
ZWaveJSNodeStatus,
|
||||
} from "../../../../../data/zwave_js";
|
||||
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) {
|
||||
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 _nodeStatuses: Record<number, ZWaveJSNodeStatus> = {};
|
||||
|
||||
@state() private _nodeStatistics: Record<
|
||||
number,
|
||||
ZWaveJSNodeStatisticsUpdatedMessage
|
||||
> = {};
|
||||
|
||||
@state() private _devices: Record<string, DeviceRegistryEntry> = {};
|
||||
|
||||
public hassSubscribe() {
|
||||
const devices = Object.values(this.hass.devices).filter((device) =>
|
||||
device.config_entries.some((entry) => entry === this.configEntryId)
|
||||
);
|
||||
|
||||
return devices.map((device) =>
|
||||
subscribeZwaveNodeStatistics(this.hass!, device.id, (message) => {
|
||||
const nodeId = message.nodeId ?? message.node_id;
|
||||
this._devices[nodeId!] = device;
|
||||
this._nodeStatistics[nodeId!] = message;
|
||||
this._handleUpdatedNodeStatistics();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._fetchNetworkStatus();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${configTabs}
|
||||
>
|
||||
<ha-network-graph
|
||||
.hass=${this.hass}
|
||||
.data=${this._getNetworkData(
|
||||
this._nodeStatuses,
|
||||
this._nodeStatistics
|
||||
)}
|
||||
.tooltipFormatter=${this._tooltipFormatter}
|
||||
@chart-click=${this._handleChartClick}
|
||||
></ha-network-graph
|
||||
></hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchNetworkStatus() {
|
||||
const network = await fetchZwaveNetworkStatus(this.hass!, {
|
||||
entry_id: this.configEntryId,
|
||||
});
|
||||
const nodeStatuses: Record<number, ZWaveJSNodeStatus> = {};
|
||||
network.controller.nodes.forEach((node) => {
|
||||
nodeStatuses[node.node_id] = node;
|
||||
});
|
||||
|
||||
this._nodeStatuses = nodeStatuses;
|
||||
}
|
||||
|
||||
private _tooltipFormatter = (params: TopLevelFormatterParams): string => {
|
||||
const { dataType, data } = params as CallbackDataParams;
|
||||
if (dataType === "edge") {
|
||||
const { source, target, value } = 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 += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.data_rate")}:</b> ${this.hass.localize(`ui.panel.config.zwave_js.protocol_data_rate.${route.protocol_data_rate}`)}`;
|
||||
}
|
||||
if (value) {
|
||||
tip += `<br><b>RSSI:</b> ${value}`;
|
||||
}
|
||||
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 += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.node_id")}:</b> ${id}`;
|
||||
if (device) {
|
||||
tip += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.manufacturer")}:</b> ${device.manufacturer || "-"}`;
|
||||
tip += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.model")}:</b> ${device.model || "-"}`;
|
||||
}
|
||||
if (nodeStatus) {
|
||||
tip += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.status")}:</b> ${this.hass.localize(`ui.panel.config.zwave_js.node_status.${nodeStatus.status}`)}`;
|
||||
if (nodeStatus.zwave_plus_version) {
|
||||
tip += `<br><b>Z-Wave Plus:</b> ${this.hass.localize("ui.panel.config.zwave_js.visualization.version")} ${nodeStatus.zwave_plus_version}`;
|
||||
}
|
||||
}
|
||||
return tip;
|
||||
};
|
||||
|
||||
private _getNetworkData = memoizeOne(
|
||||
(
|
||||
nodeStatuses: Record<number, ZWaveJSNodeStatus>,
|
||||
nodeStatistics: Record<number, ZWaveJSNodeStatisticsUpdatedMessage>
|
||||
): NetworkData => {
|
||||
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"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: this.hass.localize(
|
||||
"ui.panel.config.zwave_js.visualization.asleep_node"
|
||||
),
|
||||
symbol: "circle",
|
||||
itemStyle: {
|
||||
color: colorVariables["disabled-color"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: this.hass.localize(
|
||||
"ui.panel.config.zwave_js.visualization.dead_node"
|
||||
),
|
||||
symbol: "circle",
|
||||
itemStyle: {
|
||||
color: colorVariables["error-color"],
|
||||
},
|
||||
},
|
||||
];
|
||||
if (!Object.keys(nodeStatuses).length) {
|
||||
return { nodes, links, categories };
|
||||
}
|
||||
|
||||
let controllerNode: number | undefined;
|
||||
Object.values(nodeStatuses).forEach((node) => {
|
||||
if (node.is_controller_node) {
|
||||
controllerNode = node.node_id;
|
||||
}
|
||||
const device = this._devices[node.node_id];
|
||||
nodes.push({
|
||||
id: String(node.node_id),
|
||||
name: device?.name_by_user ?? device?.name ?? String(node.node_id),
|
||||
value: node.is_controller_node ? 3 : node.is_routing ? 2 : 1,
|
||||
category:
|
||||
node.status === NodeStatus.Dead
|
||||
? 3
|
||||
: node.status === NodeStatus.Asleep
|
||||
? 2
|
||||
: 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:
|
||||
node.status === NodeStatus.Dead
|
||||
? colorVariables["error-color"]
|
||||
: node.status === NodeStatus.Asleep
|
||||
? colorVariables["disabled-color"]
|
||||
: node.is_controller_node
|
||||
? 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,
|
||||
});
|
||||
});
|
||||
|
||||
Object.entries(nodeStatistics).forEach(([nodeId, stats]) => {
|
||||
const route = stats.lwr || stats.nlwr;
|
||||
if (route) {
|
||||
const hops = [
|
||||
...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: 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),
|
||||
type:
|
||||
route.protocol_data_rate > 1
|
||||
? "solid"
|
||||
: existingLink.lineStyle!.type,
|
||||
};
|
||||
} else {
|
||||
links.push({
|
||||
source: sourceNode,
|
||||
target: String(repeater),
|
||||
value: RSSI,
|
||||
lineStyle: {
|
||||
width,
|
||||
color:
|
||||
repeater === controllerNode
|
||||
? colorVariables["primary-color"]
|
||||
: colorVariables["disabled-color"],
|
||||
type: route.protocol_data_rate > 1 ? "solid" : "dotted",
|
||||
},
|
||||
symbolSize: width * 3,
|
||||
});
|
||||
}
|
||||
sourceNode = String(repeater);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return { nodes, links, categories };
|
||||
}
|
||||
);
|
||||
|
||||
private _handleUpdatedNodeStatistics = debounce(() => {
|
||||
// all the node events come in at once, so we need to debounce to avoid
|
||||
// unnecessary re-renders
|
||||
this._nodeStatistics = { ...this._nodeStatistics };
|
||||
}, 500);
|
||||
|
||||
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 _getLineWidth(rssi: number): number {
|
||||
return rssi > -50 ? 3 : rssi > -75 ? 2 : 1;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
css`
|
||||
ha-network-graph {
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"zwave_js-network-visualization": ZWaveJSNetworkVisualization;
|
||||
}
|
||||
}
|
@ -5875,7 +5875,8 @@
|
||||
"zwave_js": {
|
||||
"navigation": {
|
||||
"network": "Network",
|
||||
"logs": "Logs"
|
||||
"logs": "Logs",
|
||||
"visualization": "Visualization"
|
||||
},
|
||||
"common": {
|
||||
"network": "Network",
|
||||
@ -6325,6 +6326,33 @@
|
||||
"log_level_changed": "Log level changed to: {level}",
|
||||
"download_logs": "Download logs"
|
||||
},
|
||||
"visualization": {
|
||||
"controller": "Controller",
|
||||
"node": "Node",
|
||||
"asleep_node": "Asleep 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",
|
||||
"introduction": "Configure your device installer settings.",
|
||||
|
Loading…
x
Reference in New Issue
Block a user