mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-19 15:26:36 +00:00
Add network visualization to the ZHA config panel (#7802)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
8a86beff14
commit
f093bd115c
@ -123,6 +123,8 @@
|
|||||||
"superstruct": "^0.10.12",
|
"superstruct": "^0.10.12",
|
||||||
"tinykeys": "^1.1.1",
|
"tinykeys": "^1.1.1",
|
||||||
"unfetch": "^4.1.0",
|
"unfetch": "^4.1.0",
|
||||||
|
"vis-data": "^7.1.1",
|
||||||
|
"vis-network": "^8.5.4",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
"vue2-daterange-picker": "^0.5.1",
|
"vue2-daterange-picker": "^0.5.1",
|
||||||
"web-animations-js": "^2.3.2",
|
"web-animations-js": "^2.3.2",
|
||||||
|
@ -7,6 +7,7 @@ export interface ZHAEntityReference extends HassEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ZHADevice {
|
export interface ZHADevice {
|
||||||
|
available: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
ieee: string;
|
ieee: string;
|
||||||
nwk: string;
|
nwk: string;
|
||||||
@ -25,6 +26,13 @@ export interface ZHADevice {
|
|||||||
area_id?: string;
|
area_id?: string;
|
||||||
device_type: string;
|
device_type: string;
|
||||||
signature: any;
|
signature: any;
|
||||||
|
neighbors: Neighbor[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Neighbor {
|
||||||
|
ieee: string;
|
||||||
|
nwk: string;
|
||||||
|
lqi: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ZHADeviceEndpoint {
|
export interface ZHADeviceEndpoint {
|
||||||
|
@ -42,6 +42,10 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
|
|||||||
tag: "zha-add-group-page",
|
tag: "zha-add-group-page",
|
||||||
load: () => import("./zha-add-group-page"),
|
load: () => import("./zha-add-group-page"),
|
||||||
},
|
},
|
||||||
|
visualization: {
|
||||||
|
tag: "zha-network-visualization-page",
|
||||||
|
load: () => import("./zha-network-visualization-page"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ import "../../../../../components/ha-icon-next";
|
|||||||
import { haStyle } from "../../../../../resources/styles";
|
import { haStyle } from "../../../../../resources/styles";
|
||||||
import type { HomeAssistant, Route } from "../../../../../types";
|
import type { HomeAssistant, Route } from "../../../../../types";
|
||||||
import "../../../ha-config-section";
|
import "../../../ha-config-section";
|
||||||
import { mdiNetwork, mdiFolderMultipleOutline, mdiPlus } from "@mdi/js";
|
import { mdiNetwork, mdiFolderMultipleOutline, mdiPlus, mdiLan } from "@mdi/js";
|
||||||
import "../../../../../layouts/hass-tabs-subpage";
|
import "../../../../../layouts/hass-tabs-subpage";
|
||||||
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||||
import { computeRTL } from "../../../../../common/util/compute_rtl";
|
import { computeRTL } from "../../../../../common/util/compute_rtl";
|
||||||
@ -32,6 +32,11 @@ export const zhaTabs: PageNavigation[] = [
|
|||||||
path: `/config/zha/groups`,
|
path: `/config/zha/groups`,
|
||||||
iconPath: mdiFolderMultipleOutline,
|
iconPath: mdiFolderMultipleOutline,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
translationKey: "ui.panel.config.zha.visualization.caption",
|
||||||
|
path: `/config/zha/visualization`,
|
||||||
|
iconPath: mdiLan,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@customElement("zha-config-dashboard")
|
@customElement("zha-config-dashboard")
|
||||||
|
@ -0,0 +1,214 @@
|
|||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
internalProperty,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
PropertyValues,
|
||||||
|
query,
|
||||||
|
} from "lit-element";
|
||||||
|
|
||||||
|
import { navigate } from "../../../../../common/navigate";
|
||||||
|
import { fetchDevices, ZHADevice } from "../../../../../data/zha";
|
||||||
|
import "../../../../../layouts/hass-subpage";
|
||||||
|
import type { HomeAssistant } from "../../../../../types";
|
||||||
|
import { Network, Edge, Node, EdgeOptions } from "vis-network";
|
||||||
|
|
||||||
|
@customElement("zha-network-visualization-page")
|
||||||
|
export class ZHANetworkVisualizationPage extends LitElement {
|
||||||
|
@property({ type: Object }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow!: boolean;
|
||||||
|
|
||||||
|
@query("#visualization", true)
|
||||||
|
private _visualization?: HTMLElement;
|
||||||
|
|
||||||
|
@internalProperty()
|
||||||
|
private _devices: Map<string, ZHADevice> = new Map();
|
||||||
|
|
||||||
|
@internalProperty()
|
||||||
|
private _network?: Network;
|
||||||
|
|
||||||
|
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||||
|
super.firstUpdated(changedProperties);
|
||||||
|
if (this.hass) {
|
||||||
|
this._fetchData();
|
||||||
|
}
|
||||||
|
this._network = new Network(
|
||||||
|
this._visualization!,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
autoResize: true,
|
||||||
|
height: window.innerHeight + "px",
|
||||||
|
width: window.innerWidth + "px",
|
||||||
|
layout: {
|
||||||
|
improvedLayout: true,
|
||||||
|
},
|
||||||
|
physics: {
|
||||||
|
barnesHut: {
|
||||||
|
springConstant: 0,
|
||||||
|
avoidOverlap: 10,
|
||||||
|
damping: 0.09,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nodes: {
|
||||||
|
font: {
|
||||||
|
multi: "html",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
edges: {
|
||||||
|
smooth: {
|
||||||
|
enabled: true,
|
||||||
|
type: "continuous",
|
||||||
|
forceDirection: "none",
|
||||||
|
roundness: 0.6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this._network.on("doubleClick", (properties) => {
|
||||||
|
const ieee = properties.nodes[0];
|
||||||
|
if (ieee) {
|
||||||
|
const device = this._devices.get(ieee);
|
||||||
|
if (device) {
|
||||||
|
navigate(
|
||||||
|
this,
|
||||||
|
`/config/devices/device/${device.device_reg_id}`,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<hass-subpage
|
||||||
|
.hass=${this.hass}
|
||||||
|
.header=${this.hass.localize(
|
||||||
|
"ui.panel.config.zha.visualization.header"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div id="visualization"></div>
|
||||||
|
</hass-subpage>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchData() {
|
||||||
|
const devices = await fetchDevices(this.hass!);
|
||||||
|
this._devices = new Map(
|
||||||
|
devices.map((device: ZHADevice) => [device.ieee, device])
|
||||||
|
);
|
||||||
|
this._updateDevices(devices);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateDevices(devices: ZHADevice[]) {
|
||||||
|
const nodes: Node[] = [];
|
||||||
|
const edges: Edge[] = [];
|
||||||
|
|
||||||
|
devices.forEach((device) => {
|
||||||
|
nodes.push({
|
||||||
|
id: device.ieee,
|
||||||
|
label: this._buildLabel(device),
|
||||||
|
shape: this._getShape(device),
|
||||||
|
mass: this._getMass(device),
|
||||||
|
});
|
||||||
|
if (device.neighbors && device.neighbors.length > 0) {
|
||||||
|
device.neighbors.forEach((neighbor) => {
|
||||||
|
const idx = edges.findIndex(function (e) {
|
||||||
|
return device.ieee === e.to && neighbor.ieee === e.from;
|
||||||
|
});
|
||||||
|
if (idx === -1) {
|
||||||
|
edges.push({
|
||||||
|
from: device.ieee,
|
||||||
|
to: neighbor.ieee,
|
||||||
|
label: neighbor.lqi + "",
|
||||||
|
color: this._getLQI(neighbor.lqi),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
edges[idx].color = this._getLQI(
|
||||||
|
(parseInt(edges[idx].label!) + neighbor.lqi) / 2
|
||||||
|
);
|
||||||
|
edges[idx].label += "/" + neighbor.lqi;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._network?.setData({ nodes: nodes, edges: edges });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getLQI(lqi: number): EdgeOptions["color"] {
|
||||||
|
if (lqi > 192) {
|
||||||
|
return { color: "#17ab00", highlight: "#17ab00" };
|
||||||
|
}
|
||||||
|
if (lqi > 128) {
|
||||||
|
return { color: "#e6b402", highlight: "#e6b402" };
|
||||||
|
}
|
||||||
|
if (lqi > 80) {
|
||||||
|
return { color: "#fc4c4c", highlight: "#fc4c4c" };
|
||||||
|
}
|
||||||
|
return { color: "#bfbfbf", highlight: "#bfbfbf" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getMass(device: ZHADevice): number {
|
||||||
|
if (device.device_type === "Coordinator") {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (device.device_type === "Router") {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getShape(device: ZHADevice): string {
|
||||||
|
if (device.device_type === "Coordinator") {
|
||||||
|
return "box";
|
||||||
|
}
|
||||||
|
if (device.device_type === "Router") {
|
||||||
|
return "ellipse";
|
||||||
|
}
|
||||||
|
return "circle";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _buildLabel(device: ZHADevice): string {
|
||||||
|
let label =
|
||||||
|
device.user_given_name !== null
|
||||||
|
? `<b>${device.user_given_name}</b>\n`
|
||||||
|
: "";
|
||||||
|
label += `<b>IEEE: </b>${device.ieee}`;
|
||||||
|
label += `\n<b>Device Type: </b>${device.device_type.replace("_", " ")}`;
|
||||||
|
if (device.nwk != null) {
|
||||||
|
label += `\n<b>NWK: </b>${device.nwk}`;
|
||||||
|
}
|
||||||
|
if (device.manufacturer != null && device.model != null) {
|
||||||
|
label += `\n<b>Device: </b>${device.manufacturer} ${device.model}`;
|
||||||
|
} else {
|
||||||
|
label += "\n<b>Device is not in <i>'zigbee.db'</i></b>";
|
||||||
|
}
|
||||||
|
if (!device.available) {
|
||||||
|
label += "\n<b>Device is <i>Offline</i></b>";
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
css`
|
||||||
|
.header {
|
||||||
|
font-family: var(--paper-font-display1_-_font-family);
|
||||||
|
-webkit-font-smoothing: var(
|
||||||
|
--paper-font-display1_-_-webkit-font-smoothing
|
||||||
|
);
|
||||||
|
font-size: var(--paper-font-display1_-_font-size);
|
||||||
|
font-weight: var(--paper-font-display1_-_font-weight);
|
||||||
|
letter-spacing: var(--paper-font-display1_-_letter-spacing);
|
||||||
|
line-height: var(--paper-font-display1_-_line-height);
|
||||||
|
opacity: var(--dark-primary-opacity);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -2215,6 +2215,10 @@
|
|||||||
"create": "Create Group",
|
"create": "Create Group",
|
||||||
"creating_group": "Creating Group"
|
"creating_group": "Creating Group"
|
||||||
},
|
},
|
||||||
|
"visualization": {
|
||||||
|
"header": "Network Visualization",
|
||||||
|
"caption": "Visualization"
|
||||||
|
},
|
||||||
"group_binding": {
|
"group_binding": {
|
||||||
"header": "Group Binding",
|
"header": "Group Binding",
|
||||||
"introduction": "Bind and unbind groups.",
|
"introduction": "Bind and unbind groups.",
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -13567,6 +13567,16 @@ vinyl@^2.0.0, vinyl@^2.1.0:
|
|||||||
remove-trailing-separator "^1.0.1"
|
remove-trailing-separator "^1.0.1"
|
||||||
replace-ext "^1.0.0"
|
replace-ext "^1.0.0"
|
||||||
|
|
||||||
|
vis-data@^7.1.1:
|
||||||
|
version "7.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/vis-data/-/vis-data-7.1.1.tgz#a38b38efcc0b721181ddaa4f4359edfd75623825"
|
||||||
|
integrity sha512-Z5+caySDqoKL9yxbI3c/CKmUcSvROSZstuvwxbOsUpdxHpxFYEUgxC1EH4lSB1ykEaM54MVMM1UcwB9oNaWFlw==
|
||||||
|
|
||||||
|
vis-network@^8.5.4:
|
||||||
|
version "8.5.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/vis-network/-/vis-network-8.5.4.tgz#6979a626c28f29e981adf535a8e6c25503c26f7f"
|
||||||
|
integrity sha512-KeYHlTZpbPHS6868MHnMtRXDTmKA0YwQQl/mC5cBiICGH67ilzOqkyWObAMyeo8b8Z/6pTfFJEu9g70EvWqOYA==
|
||||||
|
|
||||||
vm-browserify@0.0.4:
|
vm-browserify@0.0.4:
|
||||||
version "0.0.4"
|
version "0.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"
|
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user