Compare commits

...
3 changed files with 126 additions and 4 deletions
+10
View File
@@ -526,6 +526,16 @@ export const fetchZwaveNetworkStatus = (
});
};
/** Node IDs of the nodes each node can reach directly, keyed by node ID. */
export const fetchZwaveNetworkNeighbors = (
hass: HomeAssistant,
entry_id: string
): Promise<Record<number, number[]>> =>
hass.callWS({
type: "zwave_js/network_neighbors",
entry_id,
});
export const fetchZwaveDataCollectionStatus = (
hass: HomeAssistant,
entry_id: string
@@ -1,3 +1,4 @@
import { mdiSpiderWeb } from "@mdi/js";
import type {
CallbackDataParams,
TopLevelFormatterParams,
@@ -16,6 +17,7 @@ import type {
NetworkLink,
NetworkNode,
} from "../../../../../components/chart/ha-network-graph";
import "../../../../../components/ha-icon-button";
import "../../../../../components/input/ha-input-search";
import type { HaInputSearch } from "../../../../../components/input/ha-input-search";
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
@@ -25,6 +27,7 @@ import type {
ZWaveJSNodeStatus,
} from "../../../../../data/zwave_js";
import {
fetchZwaveNetworkNeighbors,
fetchZwaveNetworkStatus,
getNodeIdFromDevice,
NodeStatus,
@@ -33,6 +36,7 @@ import {
import "../../../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
import type { HomeAssistant, Route } from "../../../../../types";
import { showToast } from "../../../../../util/toast";
@customElement("zwave_js-network-visualization")
export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
@@ -55,11 +59,19 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
@state() private _devices: Record<string, DeviceRegistryEntry> = {};
@state() private _neighbors?: Record<number, number[]>;
@state() private _showNeighbors = false;
@state() private _searchFilter = "";
// Route statistics reference repeaters by device registry ID
private _nodeIdsByDeviceId: Record<string, number> = {};
private _neighborLinks = new Set<string>();
private _loadingNeighbors = false;
public hassSubscribe() {
const subscriptions: Promise<UnsubscribeFunc>[] = [];
const devices: Record<number, DeviceRegistryEntry> = {};
@@ -89,6 +101,32 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
return subscriptions;
}
private async _toggleNeighbors() {
this._showNeighbors = !this._showNeighbors;
if (!this._showNeighbors || this._neighbors || this._loadingNeighbors) {
return;
}
// fetched on demand: reading neighbors turns the radio off briefly
this._loadingNeighbors = true;
try {
this._neighbors = await fetchZwaveNetworkNeighbors(
this.hass,
this.configEntryId
);
} catch (err: unknown) {
this._showNeighbors = false;
showToast(this, {
message:
(err as { message?: string }).message ??
this.hass.localize(
"ui.panel.config.zwave_js.visualization.neighbors_error"
),
});
} finally {
this._loadingNeighbors = false;
}
}
public connectedCallback() {
super.connectedCallback();
this._fetchNetworkStatus();
@@ -116,13 +154,23 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
.searchFilter=${this._searchFilter}
.data=${this._getNetworkData(
this._nodeStatuses,
this._nodeStatistics
this._nodeStatistics,
this._showNeighbors ? this._neighbors : undefined
)}
.searchableAttributes=${this._getSearchableAttributes}
.tooltipFormatter=${this._tooltipFormatter}
@chart-click=${this._handleChartClick}
>
${!this.narrow ? this._renderInputSearch("search") : nothing}
<ha-icon-button
slot="button"
class=${this._showNeighbors ? "active" : "inactive"}
.path=${mdiSpiderWeb}
.label=${this.hass.localize(
"ui.panel.config.zwave_js.visualization.toggle_neighbors"
)}
@click=${this._toggleNeighbors}
></ha-icon-button>
</ha-network-graph>
</hass-subpage>
`;
@@ -184,6 +232,13 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
sourceDevice?.name_by_user ?? sourceDevice?.name ?? source;
const targetName =
targetDevice?.name_by_user ?? targetDevice?.name ?? target;
if (this._neighborLinks.has(`${source}>${target}`)) {
return html`${sourceName}${targetName}<br /><b
>${this.hass.localize(
"ui.panel.config.zwave_js.visualization.neighbor"
)}</b
>`;
}
// links point away from the controller, so the route belongs to the target
const stats =
this._nodeStatistics[target] ?? this._nodeStatistics[source];
@@ -263,7 +318,8 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
private _getNetworkData = memoizeOne(
(
nodeStatuses: Record<number, ZWaveJSNodeStatus>,
nodeStatistics: Record<number, ZWaveJSNodeStatisticsUpdatedMessage>
nodeStatistics: Record<number, ZWaveJSNodeStatisticsUpdatedMessage>,
neighbors: Record<number, number[]> | undefined
): NetworkData => {
const style = getComputedStyle(this);
const nodes: NetworkNode[] = [];
@@ -420,7 +476,49 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
});
});
return { nodes, links, categories };
// Neighbors are the nodes a node can reach directly. They are symmetric
// and carry no signal information, so they fill in the mesh underneath
// the measured routes without overriding them.
const neighborLinks: NetworkLink[] = [];
const neighborKeys = new Set<string>();
Object.entries(neighbors ?? {}).forEach(([nodeId, neighborIds]) => {
neighborIds.forEach((neighborId) => {
const target = String(neighborId);
if (!nodeStatuses[neighborId] || target === nodeId) {
return;
}
const [a, b] = [nodeId, target].sort();
const key = `${a}>${b}`;
if (
neighborKeys.has(key) ||
links.some(
(link) =>
(link.source === nodeId && link.target === target) ||
(link.source === target && link.target === nodeId)
)
) {
return;
}
neighborKeys.add(key);
neighborLinks.push({
source: a,
target: b,
// equal values in both directions render the link without an arrow
value: 1,
reverseValue: 1,
lineStyle: {
width: 1,
color: style.getPropertyValue("--disabled-color"),
type: "dashed",
},
// neighbors are plentiful, let the routes shape the layout
ignoreForceLayout: true,
});
});
});
this._neighborLinks = neighborKeys;
return { nodes, links: [...neighborLinks, ...links], categories };
}
);
@@ -460,6 +558,17 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
ha-input-search {
flex: 1;
}
/* ha-chart-base can't style re-slotted buttons, so mirror its look */
ha-icon-button[slot="button"] {
background: var(--card-background-color);
border-radius: var(--ha-border-radius-sm);
--ha-icon-button-size: 32px;
color: var(--primary-color);
border: 1px solid var(--divider-color);
}
ha-icon-button[slot="button"].inactive {
color: var(--state-inactive-color);
}
`,
];
}
+4 -1
View File
@@ -8321,7 +8321,10 @@
"status": "Status",
"version": "Version",
"data_rate": "Data rate",
"area": "Area"
"area": "Area",
"neighbor": "In range of each other",
"toggle_neighbors": "Toggle neighbor connections",
"neighbors_error": "Failed to load neighbor connections"
},
"node_status": {
"0": "Unknown",