mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-22 00:36:34 +00:00
Bluetooth network visualization (#25512)
* Bluetooth network visualization * fix category symbol * Add translations * memoize bluetooth data * throttle data updates to 10s * handle proxies that appear as end devices too * fix tab highlighting * memoize fix
This commit is contained in:
parent
97e0217906
commit
07e5f53469
@ -3,7 +3,7 @@ import type { GraphSeriesOption } from "echarts/charts";
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state, query } from "lit/decorators";
|
import { customElement, property, state, query } from "lit/decorators";
|
||||||
import type { TopLevelFormatterParams } from "echarts/types/dist/shared";
|
import type { TopLevelFormatterParams } from "echarts/types/dist/shared";
|
||||||
import { mdiGoogleCirclesGroup } from "@mdi/js";
|
import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||||
import type { ECOption } from "../../resources/echarts";
|
import type { ECOption } from "../../resources/echarts";
|
||||||
@ -42,6 +42,7 @@ export interface NetworkLink {
|
|||||||
type?: "solid" | "dashed" | "dotted";
|
type?: "solid" | "dashed" | "dotted";
|
||||||
};
|
};
|
||||||
symbolSize?: number | number[];
|
symbolSize?: number | number[];
|
||||||
|
symbol?: string;
|
||||||
label?: {
|
label?: {
|
||||||
show?: boolean;
|
show?: boolean;
|
||||||
formatter?: string;
|
formatter?: string;
|
||||||
@ -52,7 +53,7 @@ export interface NetworkLink {
|
|||||||
export interface NetworkData {
|
export interface NetworkData {
|
||||||
nodes: NetworkNode[];
|
nodes: NetworkNode[];
|
||||||
links: NetworkLink[];
|
links: NetworkLink[];
|
||||||
categories?: { name: string }[];
|
categories?: { name: string; symbol: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
|
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
|
||||||
@ -62,7 +63,7 @@ let GraphChart: typeof import("echarts/lib/chart/graph/install");
|
|||||||
export class HaNetworkGraph extends LitElement {
|
export class HaNetworkGraph extends LitElement {
|
||||||
public chart?: EChartsType;
|
public chart?: EChartsType;
|
||||||
|
|
||||||
@property({ attribute: false }) public data?: NetworkData;
|
@property({ attribute: false }) public data!: NetworkData;
|
||||||
|
|
||||||
@property({ attribute: false }) public tooltipFormatter?: (
|
@property({ attribute: false }) public tooltipFormatter?: (
|
||||||
params: TopLevelFormatterParams
|
params: TopLevelFormatterParams
|
||||||
@ -74,6 +75,8 @@ export class HaNetworkGraph extends LitElement {
|
|||||||
|
|
||||||
@state() private _physicsEnabled = true;
|
@state() private _physicsEnabled = true;
|
||||||
|
|
||||||
|
@state() private _showLabels = true;
|
||||||
|
|
||||||
private _listeners: (() => void)[] = [];
|
private _listeners: (() => void)[] = [];
|
||||||
|
|
||||||
private _nodePositions: Record<string, { x: number; y: number }> = {};
|
private _nodePositions: Record<string, { x: number; y: number }> = {};
|
||||||
@ -117,7 +120,8 @@ export class HaNetworkGraph extends LitElement {
|
|||||||
.data=${this._getSeries(
|
.data=${this._getSeries(
|
||||||
this.data,
|
this.data,
|
||||||
this._physicsEnabled,
|
this._physicsEnabled,
|
||||||
this._reducedMotion
|
this._reducedMotion,
|
||||||
|
this._showLabels
|
||||||
)}
|
)}
|
||||||
.options=${this._createOptions(this.data?.categories)}
|
.options=${this._createOptions(this.data?.categories)}
|
||||||
height="100%"
|
height="100%"
|
||||||
@ -133,6 +137,15 @@ export class HaNetworkGraph extends LitElement {
|
|||||||
"ui.panel.config.common.graph.toggle_physics"
|
"ui.panel.config.common.graph.toggle_physics"
|
||||||
)}
|
)}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
|
<ha-icon-button
|
||||||
|
slot="button"
|
||||||
|
class=${this._showLabels ? "active" : "inactive"}
|
||||||
|
.path=${mdiFormatTextVariant}
|
||||||
|
@click=${this._toggleLabels}
|
||||||
|
label=${this.hass.localize(
|
||||||
|
"ui.panel.config.common.graph.toggle_labels"
|
||||||
|
)}
|
||||||
|
></ha-icon-button>
|
||||||
</ha-chart-base>`;
|
</ha-chart-base>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,7 +158,10 @@ export class HaNetworkGraph extends LitElement {
|
|||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
show: !!categories?.length,
|
show: !!categories?.length,
|
||||||
data: categories,
|
data: categories?.map((category) => ({
|
||||||
|
...category,
|
||||||
|
icon: category.symbol,
|
||||||
|
})),
|
||||||
top: 8,
|
top: 8,
|
||||||
},
|
},
|
||||||
dataZoom: {
|
dataZoom: {
|
||||||
@ -156,11 +172,12 @@ export class HaNetworkGraph extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
private _getSeries = memoizeOne(
|
private _getSeries = memoizeOne(
|
||||||
(data?: NetworkData, physicsEnabled?: boolean, reducedMotion?: boolean) => {
|
(
|
||||||
if (!data) {
|
data: NetworkData,
|
||||||
return [];
|
physicsEnabled: boolean,
|
||||||
}
|
reducedMotion: boolean,
|
||||||
|
showLabels: boolean
|
||||||
|
) => {
|
||||||
const containerWidth = this.clientWidth;
|
const containerWidth = this.clientWidth;
|
||||||
const containerHeight = this.clientHeight;
|
const containerHeight = this.clientHeight;
|
||||||
return [
|
return [
|
||||||
@ -172,7 +189,7 @@ export class HaNetworkGraph extends LitElement {
|
|||||||
roam: true,
|
roam: true,
|
||||||
selectedMode: "single",
|
selectedMode: "single",
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: showLabels,
|
||||||
position: "right",
|
position: "right",
|
||||||
},
|
},
|
||||||
emphasis: {
|
emphasis: {
|
||||||
@ -182,7 +199,7 @@ export class HaNetworkGraph extends LitElement {
|
|||||||
repulsion: [400, 600],
|
repulsion: [400, 600],
|
||||||
edgeLength: [200, 300],
|
edgeLength: [200, 300],
|
||||||
gravity: 0.1,
|
gravity: 0.1,
|
||||||
layoutAnimation: !reducedMotion,
|
layoutAnimation: !reducedMotion && data.nodes.length < 100,
|
||||||
},
|
},
|
||||||
edgeSymbol: ["none", "arrow"],
|
edgeSymbol: ["none", "arrow"],
|
||||||
edgeSymbolSize: 10,
|
edgeSymbolSize: 10,
|
||||||
@ -251,6 +268,10 @@ export class HaNetworkGraph extends LitElement {
|
|||||||
this._physicsEnabled = !this._physicsEnabled;
|
this._physicsEnabled = !this._physicsEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _toggleLabels() {
|
||||||
|
this._showLabels = !this._showLabels;
|
||||||
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -27,6 +27,18 @@ import "../../../../../layouts/hass-tabs-subpage-data-table";
|
|||||||
import { haStyle } from "../../../../../resources/styles";
|
import { haStyle } from "../../../../../resources/styles";
|
||||||
import type { HomeAssistant, Route } from "../../../../../types";
|
import type { HomeAssistant, Route } from "../../../../../types";
|
||||||
import { showBluetoothDeviceInfoDialog } from "./show-dialog-bluetooth-device-info";
|
import { showBluetoothDeviceInfoDialog } from "./show-dialog-bluetooth-device-info";
|
||||||
|
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||||
|
|
||||||
|
export const bluetoothAdvertisementMonitorTabs: PageNavigation[] = [
|
||||||
|
{
|
||||||
|
translationKey: "ui.panel.config.bluetooth.advertisement_monitor",
|
||||||
|
path: "advertisement-monitor",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
translationKey: "ui.panel.config.bluetooth.visualization",
|
||||||
|
path: "visualization",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
@customElement("bluetooth-advertisement-monitor")
|
@customElement("bluetooth-advertisement-monitor")
|
||||||
export class BluetoothAdvertisementMonitorPanel extends LitElement {
|
export class BluetoothAdvertisementMonitorPanel extends LitElement {
|
||||||
@ -220,6 +232,7 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement {
|
|||||||
@collapsed-changed=${this._handleCollapseChanged}
|
@collapsed-changed=${this._handleCollapseChanged}
|
||||||
filter=${this.address || ""}
|
filter=${this.address || ""}
|
||||||
clickable
|
clickable
|
||||||
|
.tabs=${bluetoothAdvertisementMonitorTabs}
|
||||||
></hass-tabs-subpage-data-table>
|
></hass-tabs-subpage-data-table>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,10 @@ class BluetoothConfigDashboardRouter extends HassRouterPage {
|
|||||||
tag: "bluetooth-connection-monitor",
|
tag: "bluetooth-connection-monitor",
|
||||||
load: () => import("./bluetooth-connection-monitor"),
|
load: () => import("./bluetooth-connection-monitor"),
|
||||||
},
|
},
|
||||||
|
visualization: {
|
||||||
|
tag: "bluetooth-network-visualization",
|
||||||
|
load: () => import("./bluetooth-network-visualization"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -106,6 +106,13 @@ export class BluetoothConfigDashboard extends LitElement {
|
|||||||
)}
|
)}
|
||||||
</ha-button></a
|
</ha-button></a
|
||||||
>
|
>
|
||||||
|
<a href="/config/bluetooth/visualization"
|
||||||
|
><ha-button>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.bluetooth.visualization"
|
||||||
|
)}
|
||||||
|
</ha-button></a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
<ha-card
|
<ha-card
|
||||||
@ -208,6 +215,10 @@ export class BluetoothConfigDashboard extends LitElement {
|
|||||||
ha-card {
|
ha-card {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,318 @@
|
|||||||
|
import { html, LitElement, css } from "lit";
|
||||||
|
import type { CSSResultGroup } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import type {
|
||||||
|
CallbackDataParams,
|
||||||
|
TopLevelFormatterParams,
|
||||||
|
} from "echarts/types/dist/shared";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import type { HomeAssistant, Route } from "../../../../../types";
|
||||||
|
import "../../../../../components/chart/ha-network-graph";
|
||||||
|
import type {
|
||||||
|
NetworkData,
|
||||||
|
NetworkNode,
|
||||||
|
NetworkLink,
|
||||||
|
} from "../../../../../components/chart/ha-network-graph";
|
||||||
|
import type {
|
||||||
|
BluetoothDeviceData,
|
||||||
|
BluetoothScannersDetails,
|
||||||
|
} from "../../../../../data/bluetooth";
|
||||||
|
import {
|
||||||
|
subscribeBluetoothAdvertisements,
|
||||||
|
subscribeBluetoothScannersDetails,
|
||||||
|
} from "../../../../../data/bluetooth";
|
||||||
|
import type { DeviceRegistryEntry } from "../../../../../data/device_registry";
|
||||||
|
import "../../../../../layouts/hass-subpage";
|
||||||
|
import { colorVariables } from "../../../../../resources/theme/color.globals";
|
||||||
|
import { navigate } from "../../../../../common/navigate";
|
||||||
|
import { bluetoothAdvertisementMonitorTabs } from "./bluetooth-advertisement-monitor";
|
||||||
|
import { relativeTime } from "../../../../../common/datetime/relative_time";
|
||||||
|
import { throttle } from "../../../../../common/util/throttle";
|
||||||
|
|
||||||
|
const UPDATE_THROTTLE_TIME = 10000;
|
||||||
|
|
||||||
|
@customElement("bluetooth-network-visualization")
|
||||||
|
export class BluetoothNetworkVisualization extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||||
|
|
||||||
|
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public route!: Route;
|
||||||
|
|
||||||
|
@state() private _data: BluetoothDeviceData[] = [];
|
||||||
|
|
||||||
|
@state() private _scanners: BluetoothScannersDetails = {};
|
||||||
|
|
||||||
|
@state() private _sourceDevices: Record<string, DeviceRegistryEntry> = {};
|
||||||
|
|
||||||
|
private _unsub_advertisements?: UnsubscribeFunc;
|
||||||
|
|
||||||
|
private _unsub_scanners?: UnsubscribeFunc;
|
||||||
|
|
||||||
|
private _throttledUpdateData = throttle((data: BluetoothDeviceData[]) => {
|
||||||
|
this._data = data;
|
||||||
|
}, UPDATE_THROTTLE_TIME);
|
||||||
|
|
||||||
|
public connectedCallback(): void {
|
||||||
|
super.connectedCallback();
|
||||||
|
if (this.hass) {
|
||||||
|
this._unsub_advertisements = subscribeBluetoothAdvertisements(
|
||||||
|
this.hass.connection,
|
||||||
|
(data) => {
|
||||||
|
if (!this._data.length) {
|
||||||
|
this._data = data;
|
||||||
|
} else {
|
||||||
|
this._throttledUpdateData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this._unsub_scanners = subscribeBluetoothScannersDetails(
|
||||||
|
this.hass.connection,
|
||||||
|
(scanners) => {
|
||||||
|
this._scanners = scanners;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const devices = Object.values(this.hass.devices);
|
||||||
|
const bluetoothDevices = devices.filter((device) =>
|
||||||
|
device.connections.find((connection) => connection[0] === "bluetooth")
|
||||||
|
);
|
||||||
|
this._sourceDevices = Object.fromEntries(
|
||||||
|
bluetoothDevices.map((device) => {
|
||||||
|
const connection = device.connections.find(
|
||||||
|
(c) => c[0] === "bluetooth"
|
||||||
|
)!;
|
||||||
|
return [connection[1], device];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
if (this._unsub_advertisements) {
|
||||||
|
this._unsub_advertisements();
|
||||||
|
this._unsub_advertisements = undefined;
|
||||||
|
}
|
||||||
|
this._throttledUpdateData.cancel();
|
||||||
|
if (this._unsub_scanners) {
|
||||||
|
this._unsub_scanners();
|
||||||
|
this._unsub_scanners = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<hass-tabs-subpage
|
||||||
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
.route=${this.route}
|
||||||
|
header=${this.hass.localize("ui.panel.config.bluetooth.visualization")}
|
||||||
|
.tabs=${bluetoothAdvertisementMonitorTabs}
|
||||||
|
>
|
||||||
|
<ha-network-graph
|
||||||
|
.hass=${this.hass}
|
||||||
|
.data=${this._formatNetworkData(this._data, this._scanners)}
|
||||||
|
.tooltipFormatter=${this._tooltipFormatter}
|
||||||
|
@chart-click=${this._handleChartClick}
|
||||||
|
></ha-network-graph>
|
||||||
|
</hass-tabs-subpage>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _formatNetworkData = memoizeOne(
|
||||||
|
(
|
||||||
|
data: BluetoothDeviceData[],
|
||||||
|
scanners: BluetoothScannersDetails
|
||||||
|
): NetworkData => {
|
||||||
|
const categories = [
|
||||||
|
{
|
||||||
|
name: this.hass.localize("ui.panel.config.bluetooth.core"),
|
||||||
|
symbol: "roundRect",
|
||||||
|
itemStyle: {
|
||||||
|
color: colorVariables["primary-color"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: this.hass.localize("ui.panel.config.bluetooth.scanners"),
|
||||||
|
symbol: "circle",
|
||||||
|
itemStyle: {
|
||||||
|
color: colorVariables["cyan-color"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: this.hass.localize("ui.panel.config.bluetooth.known_devices"),
|
||||||
|
symbol: "circle",
|
||||||
|
itemStyle: {
|
||||||
|
color: colorVariables["teal-color"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: this.hass.localize("ui.panel.config.bluetooth.unknown_devices"),
|
||||||
|
symbol: "circle",
|
||||||
|
itemStyle: {
|
||||||
|
color: colorVariables["disabled-color"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const nodes: NetworkNode[] = [
|
||||||
|
{
|
||||||
|
id: "ha",
|
||||||
|
name: this.hass.localize("ui.panel.config.bluetooth.core"),
|
||||||
|
category: 0,
|
||||||
|
value: 4,
|
||||||
|
symbol: "roundRect",
|
||||||
|
symbolSize: 40,
|
||||||
|
polarDistance: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const links: NetworkLink[] = [];
|
||||||
|
Object.values(scanners).forEach((scanner) => {
|
||||||
|
const scannerDevice = this._sourceDevices[scanner.source];
|
||||||
|
nodes.push({
|
||||||
|
id: scanner.source,
|
||||||
|
name:
|
||||||
|
scannerDevice?.name_by_user || scannerDevice?.name || scanner.name,
|
||||||
|
category: 1,
|
||||||
|
value: 5,
|
||||||
|
symbol: "circle",
|
||||||
|
symbolSize: 30,
|
||||||
|
polarDistance: 0.25,
|
||||||
|
});
|
||||||
|
links.push({
|
||||||
|
source: "ha",
|
||||||
|
target: scanner.source,
|
||||||
|
value: 0,
|
||||||
|
symbol: "none",
|
||||||
|
lineStyle: {
|
||||||
|
width: 3,
|
||||||
|
color: colorVariables["primary-color"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
data.forEach((node) => {
|
||||||
|
if (scanners[node.address]) {
|
||||||
|
// proxies sometimes appear as end devices too
|
||||||
|
links.push({
|
||||||
|
source: node.source,
|
||||||
|
target: node.address,
|
||||||
|
value: node.rssi,
|
||||||
|
symbol: "none",
|
||||||
|
lineStyle: {
|
||||||
|
width: this._getLineWidth(node.rssi),
|
||||||
|
color: colorVariables["primary-color"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const device = this._sourceDevices[node.address];
|
||||||
|
nodes.push({
|
||||||
|
id: node.address,
|
||||||
|
name: this._getBluetoothDeviceName(node.address),
|
||||||
|
value: device ? 1 : 0,
|
||||||
|
category: device ? 2 : 3,
|
||||||
|
symbolSize: 20,
|
||||||
|
});
|
||||||
|
links.push({
|
||||||
|
source: node.source,
|
||||||
|
target: node.address,
|
||||||
|
value: node.rssi,
|
||||||
|
symbol: "none",
|
||||||
|
lineStyle: {
|
||||||
|
width: this._getLineWidth(node.rssi),
|
||||||
|
color: device
|
||||||
|
? colorVariables["primary-color"]
|
||||||
|
: colorVariables["disabled-color"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return { nodes, links, categories };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private _getBluetoothDeviceName(id: string): string {
|
||||||
|
if (id === "ha") {
|
||||||
|
return this.hass.localize("ui.panel.config.bluetooth.core");
|
||||||
|
}
|
||||||
|
if (this._sourceDevices[id]) {
|
||||||
|
return (
|
||||||
|
this._sourceDevices[id]?.name_by_user ||
|
||||||
|
this._sourceDevices[id]?.name ||
|
||||||
|
id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this._scanners[id]) {
|
||||||
|
return this._scanners[id]?.name || id;
|
||||||
|
}
|
||||||
|
return this._data.find((d) => d.address === id)?.name || id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getLineWidth(rssi: number): number {
|
||||||
|
return rssi > -33 ? 3 : rssi > -66 ? 2 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _tooltipFormatter = (params: TopLevelFormatterParams): string => {
|
||||||
|
const { dataType, data } = params as CallbackDataParams;
|
||||||
|
let tooltipText = "";
|
||||||
|
if (dataType === "edge") {
|
||||||
|
const { source, target, value } = data as any;
|
||||||
|
const sourceName = this._getBluetoothDeviceName(source);
|
||||||
|
const targetName = this._getBluetoothDeviceName(target);
|
||||||
|
tooltipText = `${sourceName} → ${targetName}`;
|
||||||
|
if (source !== "ha") {
|
||||||
|
tooltipText += ` <b>${this.hass.localize("ui.panel.config.bluetooth.rssi")}:</b> ${value}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const { id: address } = data as any;
|
||||||
|
const name = this._getBluetoothDeviceName(address);
|
||||||
|
const btDevice = this._data.find((d) => d.address === address);
|
||||||
|
if (btDevice) {
|
||||||
|
tooltipText = `<b>${name}</b><br><b>${this.hass.localize("ui.panel.config.bluetooth.address")}:</b> ${address}<br><b>${this.hass.localize("ui.panel.config.bluetooth.rssi")}:</b> ${btDevice.rssi}<br><b>${this.hass.localize("ui.panel.config.bluetooth.source")}:</b> ${btDevice.source}<br><b>${this.hass.localize("ui.panel.config.bluetooth.updated")}:</b> ${relativeTime(new Date(btDevice.time * 1000), this.hass.locale)}`;
|
||||||
|
} else {
|
||||||
|
const device = this._sourceDevices[address];
|
||||||
|
if (device) {
|
||||||
|
tooltipText = `<b>${name}</b><br><b>${this.hass.localize("ui.panel.config.bluetooth.address")}:</b> ${address}`;
|
||||||
|
if (device.area_id) {
|
||||||
|
const area = this.hass.areas[device.area_id];
|
||||||
|
if (area) {
|
||||||
|
tooltipText += `<br><b>${this.hass.localize("ui.panel.config.bluetooth.area")}: </b>${area.name}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tooltipText;
|
||||||
|
};
|
||||||
|
|
||||||
|
private _handleChartClick(e: CustomEvent): void {
|
||||||
|
if (
|
||||||
|
e.detail.dataType === "node" &&
|
||||||
|
e.detail.event.target.cursor === "pointer"
|
||||||
|
) {
|
||||||
|
const { id } = e.detail.data;
|
||||||
|
const device = this._sourceDevices[id];
|
||||||
|
if (device) {
|
||||||
|
navigate(`/config/devices/device/${device.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
css`
|
||||||
|
ha-network-graph {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"bluetooth-network-visualization": BluetoothNetworkVisualization;
|
||||||
|
}
|
||||||
|
}
|
@ -54,9 +54,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
|||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
.isWide=${this.isWide}
|
.isWide=${this.isWide}
|
||||||
.route=${this.route}
|
.route=${this.route}
|
||||||
.header=${this.hass.localize(
|
header=${this.hass.localize("ui.panel.config.zha.visualization.header")}
|
||||||
"ui.panel.config.zha.visualization.header"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<ha-network-graph
|
<ha-network-graph
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@ -164,16 +162,16 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
|||||||
const categories = [
|
const categories = [
|
||||||
{
|
{
|
||||||
name: "Coordinator",
|
name: "Coordinator",
|
||||||
icon: "roundRect",
|
symbol: "roundRect",
|
||||||
itemStyle: { color: primaryColor },
|
itemStyle: { color: primaryColor },
|
||||||
},
|
},
|
||||||
{ name: "Router", icon: "circle", itemStyle: { color: routerColor } },
|
{ name: "Router", symbol: "circle", itemStyle: { color: routerColor } },
|
||||||
{
|
{
|
||||||
name: "End Device",
|
name: "End Device",
|
||||||
icon: "circle",
|
symbol: "circle",
|
||||||
itemStyle: { color: endDeviceColor },
|
itemStyle: { color: endDeviceColor },
|
||||||
},
|
},
|
||||||
{ name: "Offline", icon: "circle", itemStyle: { color: offlineColor } },
|
{ name: "Offline", symbol: "circle", itemStyle: { color: offlineColor } },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Create all the nodes and links
|
// Create all the nodes and links
|
||||||
@ -347,7 +345,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _getLQIWidth(lqi: number): number {
|
private _getLQIWidth(lqi: number): number {
|
||||||
return Math.max(1, Math.floor((lqi / 256) * 4));
|
return lqi > 200 ? 3 : lqi > 100 ? 2 : 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,6 +119,10 @@ export const getMyRedirects = (): Redirects => ({
|
|||||||
component: "bluetooth",
|
component: "bluetooth",
|
||||||
redirect: "/config/bluetooth/connection-monitor",
|
redirect: "/config/bluetooth/connection-monitor",
|
||||||
},
|
},
|
||||||
|
bluetooth_visualization: {
|
||||||
|
component: "bluetooth",
|
||||||
|
redirect: "/config/bluetooth/visualization",
|
||||||
|
},
|
||||||
config_bluetooth: {
|
config_bluetooth: {
|
||||||
component: "bluetooth",
|
component: "bluetooth",
|
||||||
redirect: "/config/bluetooth",
|
redirect: "/config/bluetooth",
|
||||||
|
@ -2113,7 +2113,8 @@
|
|||||||
"hide_url": "Hide URL",
|
"hide_url": "Hide URL",
|
||||||
"copy_link": "Copy link",
|
"copy_link": "Copy link",
|
||||||
"graph": {
|
"graph": {
|
||||||
"toggle_physics": "Toggle physics"
|
"toggle_physics": "Toggle physics",
|
||||||
|
"toggle_labels": "Toggle labels"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"updates": {
|
"updates": {
|
||||||
@ -5521,6 +5522,7 @@
|
|||||||
"connection_slot_allocations_monitor": "Connection slot allocations monitor",
|
"connection_slot_allocations_monitor": "Connection slot allocations monitor",
|
||||||
"connection_slot_allocations_monitor_details": "The connection slot allocations monitor displays the (GATT) connection slot allocations for the adapter. This adapter supports up to {slots} simultaneous connections. Each remote Bluetooth device that requires an active connection will use one connection slot while the Bluetooth device is connecting or connected.",
|
"connection_slot_allocations_monitor_details": "The connection slot allocations monitor displays the (GATT) connection slot allocations for the adapter. This adapter supports up to {slots} simultaneous connections. Each remote Bluetooth device that requires an active connection will use one connection slot while the Bluetooth device is connecting or connected.",
|
||||||
"connection_monitor": "Connection monitor",
|
"connection_monitor": "Connection monitor",
|
||||||
|
"visualization": "Visualization",
|
||||||
"used_connection_slot_allocations": "Used connection slot allocations",
|
"used_connection_slot_allocations": "Used connection slot allocations",
|
||||||
"no_connections": "No active connections",
|
"no_connections": "No active connections",
|
||||||
"no_advertisements_found": "No matching Bluetooth advertisements found",
|
"no_advertisements_found": "No matching Bluetooth advertisements found",
|
||||||
@ -5538,7 +5540,12 @@
|
|||||||
"manufacturer_data": "Manufacturer data",
|
"manufacturer_data": "Manufacturer data",
|
||||||
"service_data": "Service data",
|
"service_data": "Service data",
|
||||||
"service_uuids": "Service UUIDs",
|
"service_uuids": "Service UUIDs",
|
||||||
"copy_to_clipboard": "[%key:ui::panel::config::automation::editor::copy_to_clipboard%]"
|
"copy_to_clipboard": "[%key:ui::panel::config::automation::editor::copy_to_clipboard%]",
|
||||||
|
"area": "Area",
|
||||||
|
"core": "Home Assistant",
|
||||||
|
"scanners": "Scanners",
|
||||||
|
"known_devices": "Known devices",
|
||||||
|
"unknown_devices": "Unknown devices"
|
||||||
},
|
},
|
||||||
"dhcp": {
|
"dhcp": {
|
||||||
"title": "DHCP discovery",
|
"title": "DHCP discovery",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user