Add bluetooth advertisement manufacturer to bluetooth panel

pull/25807/head
Paul Bottein 2025-06-16 18:44:08 +02:00
parent 634e1dbde8
commit eaf6eaa31f
No known key found for this signature in database
5 changed files with 11607 additions and 5 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,17 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit"; import { load } from "js-yaml";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { storage } from "../../../../../common/decorators/storage"; import { storage } from "../../../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../../../common/dom/fire_event"; import type { HASSDomEvent } from "../../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../../common/translations/localize"; import type { LocalizeFunc } from "../../../../../common/translations/localize";
import { extractSearchParamsObject } from "../../../../../common/url/search-params";
import type { import type {
DataTableColumnContainer, DataTableColumnContainer,
RowClickedEvent, RowClickedEvent,
} from "../../../../../components/data-table/ha-data-table"; } from "../../../../../components/data-table/ha-data-table";
import { extractSearchParamsObject } from "../../../../../common/url/search-params";
import "../../../../../components/ha-fab"; import "../../../../../components/ha-fab";
import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-relative-time"; import "../../../../../components/ha-relative-time";
@ -23,11 +24,11 @@ import {
subscribeBluetoothScannersDetails, subscribeBluetoothScannersDetails,
} from "../../../../../data/bluetooth"; } from "../../../../../data/bluetooth";
import type { DeviceRegistryEntry } from "../../../../../data/device_registry"; import type { DeviceRegistryEntry } from "../../../../../data/device_registry";
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import "../../../../../layouts/hass-tabs-subpage-data-table"; 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[] = [ export const bluetoothAdvertisementMonitorTabs: PageNavigation[] = [
{ {
@ -58,6 +59,8 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement {
@state() private _sourceDevices: Record<string, DeviceRegistryEntry> = {}; @state() private _sourceDevices: Record<string, DeviceRegistryEntry> = {};
@state() private _manufacturers: Record<string, string> = {};
@storage({ @storage({
key: "bluetooth-advertisement-table-grouping", key: "bluetooth-advertisement-table-grouping",
state: false, state: false,
@ -76,6 +79,24 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement {
private _unsub_scanners?: UnsubscribeFunc; private _unsub_scanners?: UnsubscribeFunc;
public firstUpdated(_changedProperties: PropertyValues): void {
this._fetchManufacturers();
}
private async _fetchManufacturers() {
const response = await fetch("/static/bluetooth_company_identifiers.yaml");
const yamlText = await response.text();
const data = load(yamlText) as any;
this._manufacturers = (data.company_identifiers || []).reduce(
(acc, entry) => {
acc[parseInt(entry.value).toString()] = entry.name;
return acc;
},
{}
);
}
public connectedCallback(): void { public connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
if (this.hass) { if (this.hass) {
@ -188,17 +209,25 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement {
maxWidth: "60px", maxWidth: "60px",
sortable: true, sortable: true,
}, },
manufacturer: {
title: localize("ui.panel.config.bluetooth.manufacturer"),
filterable: true,
sortable: true,
defaultHidden: true,
},
}; };
return columns; return columns;
} }
); );
private _dataWithNamedSourceAndIds = memoizeOne((data) => private _dataWithNamedSourceAndIds = memoizeOne((data, manufacturers) =>
data.map((row) => { data.map((row) => {
const device = this._sourceDevices[row.address]; const device = this._sourceDevices[row.address];
const scannerDevice = this._sourceDevices[row.source]; const scannerDevice = this._sourceDevices[row.source];
const scanner = this._scanners[row.source]; const scanner = this._scanners[row.source];
const manufacturerCode = Object.keys(row.manufacturer_data)?.[0];
return { return {
...row, ...row,
id: row.address, id: row.address,
@ -210,6 +239,9 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement {
row.source, row.source,
device: device?.name_by_user || device?.name || undefined, device: device?.name_by_user || device?.name || undefined,
datetime: new Date(row.time * 1000), datetime: new Date(row.time * 1000),
manufacturer: manufacturerCode
? manufacturers[manufacturerCode]
: undefined,
}; };
}) })
); );
@ -221,7 +253,10 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement {
.narrow=${this.narrow} .narrow=${this.narrow}
.route=${this.route} .route=${this.route}
.columns=${this._columns(this.hass.localize)} .columns=${this._columns(this.hass.localize)}
.data=${this._dataWithNamedSourceAndIds(this._data)} .data=${this._dataWithNamedSourceAndIds(
this._data,
this._manufacturers
)}
.noDataText=${this.hass.localize( .noDataText=${this.hass.localize(
"ui.panel.config.bluetooth.no_advertisements_found" "ui.panel.config.bluetooth.no_advertisements_found"
)} )}
@ -249,6 +284,7 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement {
const entry = this._data.find((ent) => ent.address === ev.detail.id); const entry = this._data.find((ent) => ent.address === ev.detail.id);
showBluetoothDeviceInfoDialog(this, { showBluetoothDeviceInfoDialog(this, {
entry: entry!, entry: entry!,
manufacturers: this._manufacturers,
}); });
} }

View File

@ -49,6 +49,15 @@ class DialogBluetoothDeviceInfo extends LitElement implements HassDialog {
return nothing; return nothing;
} }
const manufacturerCode = this._params.entry.manufacturer_data
? Object.keys(this._params.entry.manufacturer_data)[0]
: undefined;
const manufacturerName =
manufacturerCode && this._params.manufacturers[manufacturerCode]
? this._params.manufacturers[manufacturerCode]
: undefined;
return html` return html`
<ha-dialog <ha-dialog
open open
@ -67,6 +76,16 @@ class DialogBluetoothDeviceInfo extends LitElement implements HassDialog {
<br /> <br />
<b>${this.hass.localize("ui.panel.config.bluetooth.source")}</b>: <b>${this.hass.localize("ui.panel.config.bluetooth.source")}</b>:
${this._params.entry.source} ${this._params.entry.source}
${manufacturerName
? html`
<br />
<b>
${this.hass.localize(
"ui.panel.config.bluetooth.manufacturer"
)}</b
>: ${manufacturerName}
`
: nothing}
</p> </p>
<h3> <h3>

View File

@ -3,6 +3,7 @@ import type { BluetoothDeviceData } from "../../../../../data/bluetooth";
export interface BluetoothDeviceInfoDialogParams { export interface BluetoothDeviceInfoDialogParams {
entry: BluetoothDeviceData; entry: BluetoothDeviceData;
manufacturers: Record<string, string>;
} }
export const loadBluetoothDeviceInfoDialog = () => export const loadBluetoothDeviceInfoDialog = () =>

View File

@ -5575,6 +5575,7 @@
"source_address": "Source address", "source_address": "Source address",
"updated": "Updated", "updated": "Updated",
"device": "Device", "device": "Device",
"manufacturer": "Manufacturer",
"device_information": "Device information", "device_information": "Device information",
"advertisement_data": "Advertisement data", "advertisement_data": "Advertisement data",
"manufacturer_data": "Manufacturer data", "manufacturer_data": "Manufacturer data",