mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-19 15:26:36 +00:00
more progress
This commit is contained in:
parent
100525735c
commit
5b74970733
@ -10,12 +10,12 @@ import type { ECOption } from "../../resources/echarts";
|
|||||||
import "./ha-chart-base";
|
import "./ha-chart-base";
|
||||||
import type { HaChartBase } from "./ha-chart-base";
|
import type { HaChartBase } from "./ha-chart-base";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||||
|
|
||||||
export interface NetworkNode {
|
export interface NetworkNode {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
category?: number;
|
category?: number;
|
||||||
label?: string;
|
|
||||||
value?: number;
|
value?: number;
|
||||||
symbolSize?: number;
|
symbolSize?: number;
|
||||||
symbol?: string;
|
symbol?: string;
|
||||||
@ -60,7 +60,7 @@ export interface NetworkData {
|
|||||||
let GraphChart: typeof import("echarts/lib/chart/graph/install");
|
let GraphChart: typeof import("echarts/lib/chart/graph/install");
|
||||||
|
|
||||||
@customElement("ha-network-graph")
|
@customElement("ha-network-graph")
|
||||||
export class HaNetworkGraph extends LitElement {
|
export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||||
public chart?: EChartsType;
|
public chart?: EChartsType;
|
||||||
|
|
||||||
@property({ attribute: false }) public data!: NetworkData;
|
@property({ attribute: false }) public data!: NetworkData;
|
||||||
@ -77,8 +77,6 @@ export class HaNetworkGraph extends LitElement {
|
|||||||
|
|
||||||
@state() private _showLabels = true;
|
@state() private _showLabels = true;
|
||||||
|
|
||||||
private _listeners: (() => void)[] = [];
|
|
||||||
|
|
||||||
private _nodePositions: Record<string, { x: number; y: number }> = {};
|
private _nodePositions: Record<string, { x: number; y: number }> = {};
|
||||||
|
|
||||||
@query("ha-chart-base") private _baseChart?: HaChartBase;
|
@query("ha-chart-base") private _baseChart?: HaChartBase;
|
||||||
@ -93,22 +91,14 @@ export class HaNetworkGraph extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async connectedCallback() {
|
protected hassSubscribe() {
|
||||||
super.connectedCallback();
|
return [
|
||||||
this._listeners.push(
|
|
||||||
listenMediaQuery("(prefers-reduced-motion)", (matches) => {
|
listenMediaQuery("(prefers-reduced-motion)", (matches) => {
|
||||||
if (this._reducedMotion !== matches) {
|
if (this._reducedMotion !== matches) {
|
||||||
this._reducedMotion = matches;
|
this._reducedMotion = matches;
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
);
|
];
|
||||||
}
|
|
||||||
|
|
||||||
public disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
while (this._listeners.length) {
|
|
||||||
this._listeners.pop()!();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
@ -122,7 +112,7 @@ export class HaNetworkGraph extends LitElement {
|
|||||||
this._physicsEnabled,
|
this._physicsEnabled,
|
||||||
this._reducedMotion,
|
this._reducedMotion,
|
||||||
this._showLabels
|
this._showLabels
|
||||||
)}
|
) as GraphSeriesOption}
|
||||||
.options=${this._createOptions(this.data?.categories)}
|
.options=${this._createOptions(this.data?.categories)}
|
||||||
height="100%"
|
height="100%"
|
||||||
.extraComponents=${[GraphChart]}
|
.extraComponents=${[GraphChart]}
|
||||||
@ -180,8 +170,7 @@ export class HaNetworkGraph extends LitElement {
|
|||||||
) => {
|
) => {
|
||||||
const containerWidth = this.clientWidth;
|
const containerWidth = this.clientWidth;
|
||||||
const containerHeight = this.clientHeight;
|
const containerHeight = this.clientHeight;
|
||||||
return [
|
return {
|
||||||
{
|
|
||||||
id: "network",
|
id: "network",
|
||||||
type: "graph",
|
type: "graph",
|
||||||
layout: physicsEnabled ? "force" : "none",
|
layout: physicsEnabled ? "force" : "none",
|
||||||
@ -204,8 +193,7 @@ export class HaNetworkGraph extends LitElement {
|
|||||||
edgeSymbol: ["none", "arrow"],
|
edgeSymbol: ["none", "arrow"],
|
||||||
edgeSymbolSize: 10,
|
edgeSymbolSize: 10,
|
||||||
data: data.nodes.map((node) => {
|
data: data.nodes.map((node) => {
|
||||||
const echartsNode: NonNullable<GraphSeriesOption["data"]>[number] =
|
const echartsNode: NonNullable<GraphSeriesOption["data"]>[number] = {
|
||||||
{
|
|
||||||
id: node.id,
|
id: node.id,
|
||||||
name: node.name,
|
name: node.name,
|
||||||
category: node.category,
|
category: node.category,
|
||||||
@ -243,12 +231,20 @@ export class HaNetworkGraph extends LitElement {
|
|||||||
symbolSize: link.reverseValue ? 1 : link.symbolSize, // 0 doesn't work
|
symbolSize: link.reverseValue ? 1 : link.symbolSize, // 0 doesn't work
|
||||||
})),
|
})),
|
||||||
categories: data.categories || [],
|
categories: data.categories || [],
|
||||||
},
|
};
|
||||||
] as any;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
private _togglePhysics() {
|
private _togglePhysics() {
|
||||||
|
this._saveNodePositions();
|
||||||
|
this._physicsEnabled = !this._physicsEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _toggleLabels() {
|
||||||
|
this._showLabels = !this._showLabels;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _saveNodePositions() {
|
||||||
if (this._baseChart?.chart) {
|
if (this._baseChart?.chart) {
|
||||||
this._baseChart.chart
|
this._baseChart.chart
|
||||||
// @ts-ignore private method but no other way to get the graph positions
|
// @ts-ignore private method but no other way to get the graph positions
|
||||||
@ -265,11 +261,6 @@ export class HaNetworkGraph extends LitElement {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this._physicsEnabled = !this._physicsEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _toggleLabels() {
|
|
||||||
this._showLabels = !this._showLabels;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
|
@ -358,6 +358,8 @@ export enum ProtocolDataRate {
|
|||||||
export interface ZWaveJSNodeStatisticsUpdatedMessage {
|
export interface ZWaveJSNodeStatisticsUpdatedMessage {
|
||||||
event: "statistics updated";
|
event: "statistics updated";
|
||||||
source: "node";
|
source: "node";
|
||||||
|
nodeId?: number;
|
||||||
|
node_id?: number;
|
||||||
commands_tx: number;
|
commands_tx: number;
|
||||||
commands_rx: number;
|
commands_rx: number;
|
||||||
commands_dropped_tx: number;
|
commands_dropped_tx: number;
|
||||||
@ -1067,3 +1069,12 @@ export const cancelSecureBootstrapS2 = (
|
|||||||
type: "zwave_js/cancel_secure_bootstrap_s2",
|
type: "zwave_js/cancel_secure_bootstrap_s2",
|
||||||
entry_id,
|
entry_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const fetchZwaveNeighbors = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry_id: string
|
||||||
|
): Promise<Record<number, number[]>> =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "zwave_js/get_neighbors",
|
||||||
|
entry_id,
|
||||||
|
});
|
@ -1,5 +1,5 @@
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import type { HomeAssistant, Route } from "../../../../../types";
|
import type { HomeAssistant, Route } from "../../../../../types";
|
||||||
import { configTabs } from "./zwave_js-config-router";
|
import { configTabs } from "./zwave_js-config-router";
|
||||||
@ -11,8 +11,18 @@ import type {
|
|||||||
} from "../../../../../components/chart/ha-network-graph";
|
} from "../../../../../components/chart/ha-network-graph";
|
||||||
import "../../../../../components/chart/ha-network-graph";
|
import "../../../../../components/chart/ha-network-graph";
|
||||||
import "../../../../../layouts/hass-tabs-subpage";
|
import "../../../../../layouts/hass-tabs-subpage";
|
||||||
import { fetchZwaveNetworkStatus } from "../../../../../data/zwave_js";
|
import {
|
||||||
|
fetchZwaveNetworkStatus,
|
||||||
|
NodeStatus,
|
||||||
|
subscribeZwaveNodeStatistics,
|
||||||
|
} from "../../../../../data/zwave_js";
|
||||||
|
import type {
|
||||||
|
ZWaveJSNodeStatisticsUpdatedMessage,
|
||||||
|
ZWaveJSNodeStatus,
|
||||||
|
} from "../../../../../data/zwave_js";
|
||||||
import { colorVariables } from "../../../../../resources/theme/color.globals";
|
import { colorVariables } from "../../../../../resources/theme/color.globals";
|
||||||
|
import type { DeviceRegistryEntry } from "../../../../../data/device_registry";
|
||||||
|
import { debounce } from "../../../../../common/util/debounce";
|
||||||
|
|
||||||
@customElement("zwave_js-network-visualization")
|
@customElement("zwave_js-network-visualization")
|
||||||
export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
||||||
@ -26,16 +36,36 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@property({ attribute: false }) public configEntryId!: string;
|
@property({ attribute: false }) public configEntryId!: string;
|
||||||
|
|
||||||
@state() private _data: NetworkData | null = null;
|
@state() private _nodeStatuses: Record<number, ZWaveJSNodeStatus> = {};
|
||||||
|
|
||||||
protected async firstUpdated() {
|
@state() private _nodeStatistics: Record<
|
||||||
this._data = await this._getNetworkData();
|
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() {
|
protected render() {
|
||||||
if (!this._data) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
return html`
|
return html`
|
||||||
<hass-tabs-subpage
|
<hass-tabs-subpage
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@ -45,14 +75,36 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
|||||||
>
|
>
|
||||||
<ha-network-graph
|
<ha-network-graph
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.data=${this._data}
|
.data=${this._getNetworkData(
|
||||||
|
this._nodeStatuses,
|
||||||
|
this._nodeStatistics
|
||||||
|
)}
|
||||||
@chart-click=${this._handleChartClick}
|
@chart-click=${this._handleChartClick}
|
||||||
></ha-network-graph
|
></ha-network-graph
|
||||||
></hass-tabs-subpage>
|
></hass-tabs-subpage>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getNetworkData = memoizeOne(async (): Promise<NetworkData> => {
|
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;
|
||||||
|
|
||||||
|
// const neighbors = await fetchZwaveNeighbors(this.hass!, this.configEntryId);
|
||||||
|
// console.log("neighbors", neighbors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getNetworkData = memoizeOne(
|
||||||
|
(
|
||||||
|
nodeStatuses: Record<number, ZWaveJSNodeStatus>,
|
||||||
|
nodeStatistics: Record<number, ZWaveJSNodeStatisticsUpdatedMessage>
|
||||||
|
): NetworkData => {
|
||||||
const nodes: NetworkNode[] = [];
|
const nodes: NetworkNode[] = [];
|
||||||
const links: NetworkLink[] = [];
|
const links: NetworkLink[] = [];
|
||||||
const categories = [
|
const categories = [
|
||||||
@ -66,28 +118,108 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: this.hass.localize("ui.panel.config.zwave_js.visualization.node"),
|
name: this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.visualization.node"
|
||||||
|
),
|
||||||
symbol: "circle",
|
symbol: "circle",
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: colorVariables["cyan-color"],
|
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"],
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
const network = await fetchZwaveNetworkStatus(this.hass!, {
|
if (!Object.keys(nodeStatuses).length) {
|
||||||
entry_id: this.configEntryId,
|
return { nodes, links, categories };
|
||||||
});
|
}
|
||||||
network.controller.nodes.forEach((node) => {
|
|
||||||
|
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({
|
nodes.push({
|
||||||
id: String(node.node_id),
|
id: String(node.node_id),
|
||||||
label: String(node.node_id),
|
name: device?.name_by_user ?? device?.name ?? String(node.node_id),
|
||||||
fixed: node.is_controller_node,
|
fixed: node.is_controller_node,
|
||||||
polarDistance: node.is_controller_node ? 0 : 0.5,
|
polarDistance: node.is_controller_node
|
||||||
category: node.is_controller_node ? 0 : 1,
|
? 0
|
||||||
|
: node.status === NodeStatus.Dead
|
||||||
|
? 1
|
||||||
|
: 0.5,
|
||||||
|
category:
|
||||||
|
node.status === NodeStatus.Dead
|
||||||
|
? 3
|
||||||
|
: node.status === NodeStatus.Asleep
|
||||||
|
? 2
|
||||||
|
: node.is_controller_node
|
||||||
|
? 0
|
||||||
|
: 1,
|
||||||
|
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"],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return { nodes, links, categories };
|
Object.entries(nodeStatistics).forEach(([nodeId, stats]) => {
|
||||||
|
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!,
|
||||||
|
];
|
||||||
|
let sourceNode = nodeId;
|
||||||
|
hops.forEach((repeater) => {
|
||||||
|
links.push({
|
||||||
|
source: String(sourceNode),
|
||||||
|
target: String(repeater),
|
||||||
});
|
});
|
||||||
|
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) {
|
private _handleChartClick(_e: CustomEvent) {
|
||||||
// @TODO
|
// @TODO
|
||||||
|
@ -6249,7 +6249,9 @@
|
|||||||
},
|
},
|
||||||
"visualization": {
|
"visualization": {
|
||||||
"controller": "Controller",
|
"controller": "Controller",
|
||||||
"node": "Node"
|
"node": "Node",
|
||||||
|
"asleep_node": "Asleep Node",
|
||||||
|
"dead_node": "Dead Node"
|
||||||
},
|
},
|
||||||
"node_installer": {
|
"node_installer": {
|
||||||
"header": "Installer Settings",
|
"header": "Installer Settings",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user