Add network visualization to the ZHA config panel (#7802)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
David F. Mulcahey 2020-11-26 17:56:50 -05:00 committed by GitHub
parent 8a86beff14
commit f093bd115c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 248 additions and 1 deletions

View File

@ -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",

View File

@ -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 {

View File

@ -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"),
},
},
};

View File

@ -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")

View File

@ -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);
}
`,
];
}
}

View File

@ -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.",

View File

@ -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"