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",
|
||||
"tinykeys": "^1.1.1",
|
||||
"unfetch": "^4.1.0",
|
||||
"vis-data": "^7.1.1",
|
||||
"vis-network": "^8.5.4",
|
||||
"vue": "^2.6.11",
|
||||
"vue2-daterange-picker": "^0.5.1",
|
||||
"web-animations-js": "^2.3.2",
|
||||
|
@ -7,6 +7,7 @@ export interface ZHAEntityReference extends HassEntity {
|
||||
}
|
||||
|
||||
export interface ZHADevice {
|
||||
available: boolean;
|
||||
name: string;
|
||||
ieee: string;
|
||||
nwk: string;
|
||||
@ -25,6 +26,13 @@ export interface ZHADevice {
|
||||
area_id?: string;
|
||||
device_type: string;
|
||||
signature: any;
|
||||
neighbors: Neighbor[];
|
||||
}
|
||||
|
||||
export interface Neighbor {
|
||||
ieee: string;
|
||||
nwk: string;
|
||||
lqi: number;
|
||||
}
|
||||
|
||||
export interface ZHADeviceEndpoint {
|
||||
|
@ -42,6 +42,10 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
|
||||
tag: "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 type { HomeAssistant, Route } from "../../../../../types";
|
||||
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 type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import { computeRTL } from "../../../../../common/util/compute_rtl";
|
||||
@ -32,6 +32,11 @@ export const zhaTabs: PageNavigation[] = [
|
||||
path: `/config/zha/groups`,
|
||||
iconPath: mdiFolderMultipleOutline,
|
||||
},
|
||||
{
|
||||
translationKey: "ui.panel.config.zha.visualization.caption",
|
||||
path: `/config/zha/visualization`,
|
||||
iconPath: mdiLan,
|
||||
},
|
||||
];
|
||||
|
||||
@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",
|
||||
"creating_group": "Creating Group"
|
||||
},
|
||||
"visualization": {
|
||||
"header": "Network Visualization",
|
||||
"caption": "Visualization"
|
||||
},
|
||||
"group_binding": {
|
||||
"header": "Group Binding",
|
||||
"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"
|
||||
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:
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"
|
||||
|
Loading…
x
Reference in New Issue
Block a user