From 94cddc91ffc01fdd2624a78123c4e3a9f09851e4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Jan 2026 14:19:42 +0100 Subject: [PATCH 1/8] Support multiple adapters in bluetooth panel --- .../bluetooth/bluetooth-config-dashboard.ts | 232 +++++++++--------- src/translations/en.json | 3 +- 2 files changed, 118 insertions(+), 117 deletions(-) diff --git a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts index 28874a5ae3d6..edf8037de077 100644 --- a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts @@ -1,11 +1,15 @@ +import { mdiCogOutline } from "@mdi/js"; import type { CSSResultGroup, TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; -import "../../../../../components/ha-card"; -import "../../../../../components/ha-code-editor"; -import "../../../../../components/ha-formfield"; -import "../../../../../components/ha-switch"; +import "../../../../../components/ha-alert"; import "../../../../../components/ha-button"; +import "../../../../../components/ha-card"; +import "../../../../../components/ha-icon-button"; +import "../../../../../components/ha-list"; +import "../../../../../components/ha-list-item"; +import "../../../../../components/ha-metric"; +import type { ConfigEntry } from "../../../../../data/config_entries"; import { getConfigEntries } from "../../../../../data/config_entries"; import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow"; import "../../../../../layouts/hass-subpage"; @@ -26,7 +30,6 @@ import { getValueInPercentage, roundWithOneDecimal, } from "../../../../../util/calculate"; -import "../../../../../components/ha-metric"; @customElement("bluetooth-config-dashboard") export class BluetoothConfigDashboard extends LitElement { @@ -34,11 +37,13 @@ export class BluetoothConfigDashboard extends LitElement { @property({ type: Boolean }) public narrow = false; + @state() private _configEntries: ConfigEntry[] = []; + @state() private _connectionAllocationData: BluetoothAllocationsData[] = []; @state() private _connectionAllocationsError?: string; - @state() private _scannerState?: BluetoothScannerState; + @state() private _scannerStates: Record = {}; @state() private _scannerDetails?: BluetoothScannersDetails; @@ -55,12 +60,19 @@ export class BluetoothConfigDashboard extends LitElement { public connectedCallback(): void { super.connectedCallback(); if (this.hass) { + this._loadConfigEntries(); this._subscribeBluetoothConnectionAllocations(); this._subscribeBluetoothScannerState(); this._subscribeScannerDetails(); } } + private async _loadConfigEntries(): Promise { + this._configEntries = await getConfigEntries(this.hass, { + domain: "bluetooth", + }); + } + private async _subscribeBluetoothConnectionAllocations(): Promise { if (this._unsubConnectionAllocations || !this._configEntry) { return; @@ -81,15 +93,17 @@ export class BluetoothConfigDashboard extends LitElement { } private async _subscribeBluetoothScannerState(): Promise { - if (this._unsubScannerState || !this._configEntry) { + if (this._unsubScannerState) { return; } this._unsubScannerState = await subscribeBluetoothScannerState( this.hass.connection, (scannerState) => { - this._scannerState = scannerState; - }, - this._configEntry + this._scannerStates = { + ...this._scannerStates, + [scannerState.source]: scannerState, + }; + } ); } @@ -122,13 +136,6 @@ export class BluetoothConfigDashboard extends LitElement { } protected render(): TemplateResult { - // Get scanner type to determine if options button should be shown - const scannerDetails = - this._scannerState && this._scannerDetails?.[this._scannerState.source]; - const scannerType: HaScannerType = - scannerDetails?.scanner_type ?? "unknown"; - const isRemoteScanner = scannerType === "remote"; - return html`
@@ -137,16 +144,7 @@ export class BluetoothConfigDashboard extends LitElement { "ui.panel.config.bluetooth.settings_title" )} > -
${this._renderScannerState()}
- ${!isRemoteScanner - ? html`
- ${this.hass.localize( - "ui.panel.config.bluetooth.option_flow" - )} -
` - : nothing} + ${this._renderAdaptersList()} roundWithOneDecimal(getValueInPercentage(used, 0, total)); + private _renderAdaptersList() { + if (this._configEntries.length === 0) { + return html` + ${this.hass.localize( + "ui.panel.config.bluetooth.no_scanner_state_available" + )} + `; + } + + return this._configEntries.map((entry) => { + // Find scanner state by matching scanner details name to config entry title + const scannerState = Object.values(this._scannerStates).find( + (s) => this._scannerDetails?.[s.source]?.name === entry.title + ); + const scannerDetails = scannerState + ? this._scannerDetails?.[scannerState.source] + : undefined; + const scannerType: HaScannerType = + scannerDetails?.scanner_type ?? "unknown"; + const isRemoteScanner = scannerType === "remote"; + const hasMismatch = + scannerState && + scannerState.current_mode !== scannerState.requested_mode; + + const secondaryText = this._formatScannerModeText(scannerState); + + return html` + + ${entry.title} + ${secondaryText} +
${secondaryText}
+ ${!isRemoteScanner + ? html`` + : nothing} +
+ ${hasMismatch + ? this._renderScannerMismatchWarning(scannerState, scannerType) + : nothing} + `; + }); + } + private _renderScannerMismatchWarning( scannerState: BluetoothScannerState, - scannerType: HaScannerType, - formatMode: (mode: string | null) => string + scannerType: HaScannerType ) { const instructions: string[] = []; @@ -238,8 +285,8 @@ export class BluetoothConfigDashboard extends LitElement { ${this.hass.localize( "ui.panel.config.bluetooth.scanner_mode_mismatch", { - requested: formatMode(scannerState.requested_mode), - current: formatMode(scannerState.current_mode), + requested: this._formatMode(scannerState.requested_mode), + current: this._formatMode(scannerState.current_mode), } )}
@@ -249,71 +296,41 @@ export class BluetoothConfigDashboard extends LitElement { `; } - private _renderScannerState() { - if (!this._configEntry || !this._scannerState) { - return html`
- ${this.hass.localize( - "ui.panel.config.bluetooth.no_scanner_state_available" - )} -
`; + private _formatMode(mode: string | null): string { + switch (mode) { + case null: + return this.hass.localize( + "ui.panel.config.bluetooth.scanning_mode_none" + ); + case "active": + return this.hass.localize( + "ui.panel.config.bluetooth.scanning_mode_active" + ); + case "passive": + return this.hass.localize( + "ui.panel.config.bluetooth.scanning_mode_passive" + ); + default: + return mode; } + } - const scannerState = this._scannerState; - // Find the scanner details for this source - const scannerDetails = this._scannerDetails?.[scannerState.source]; - const scannerType: HaScannerType = - scannerDetails?.scanner_type ?? "unknown"; - - const formatMode = (mode: string | null) => { - switch (mode) { - case null: - return this.hass.localize( - "ui.panel.config.bluetooth.scanning_mode_none" - ); - case "active": - return this.hass.localize( - "ui.panel.config.bluetooth.scanning_mode_active" - ); - case "passive": - return this.hass.localize( - "ui.panel.config.bluetooth.scanning_mode_passive" - ); - default: - return mode; // Fallback for unknown modes - } - }; + private _formatScannerModeText( + scannerState: BluetoothScannerState | undefined + ): string { + if (!scannerState) { + return this.hass.localize( + "ui.panel.config.bluetooth.scanner_state_unknown" + ); + } - return html` -
-
- ${this.hass.localize( - "ui.panel.config.bluetooth.current_scanning_mode" - )}: - ${formatMode(scannerState.current_mode)} -
-
- ${this.hass.localize( - "ui.panel.config.bluetooth.requested_scanning_mode" - )}: - ${formatMode(scannerState.requested_mode)} -
- ${scannerState.current_mode !== scannerState.requested_mode - ? this._renderScannerMismatchWarning( - scannerState, - scannerType, - formatMode - ) - : nothing} -
- `; + const currentMode = this._formatMode(scannerState.current_mode); + + if (scannerState.current_mode === scannerState.requested_mode) { + return currentMode; + } + + return `${currentMode} (${this._formatMode(scannerState.requested_mode)})`; } private _renderConnectionAllocations() { @@ -358,18 +375,9 @@ export class BluetoothConfigDashboard extends LitElement { `; } - private async _openOptionFlow() { - const configEntryId = this._configEntry; - if (!configEntryId) { - return; - } - const configEntries = await getConfigEntries(this.hass, { - domain: "bluetooth", - }); - const configEntry = configEntries.find( - (entry) => entry.entry_id === configEntryId - ); - showOptionsFlowDialog(this, configEntry!); + private _openOptionFlow(ev: Event) { + const button = ev.currentTarget as HTMLElement & { entry: ConfigEntry }; + showOptionsFlowDialog(this, button.entry); } static get styles(): CSSResultGroup { @@ -394,17 +402,9 @@ export class BluetoothConfigDashboard extends LitElement { display: flex; justify-content: flex-end; } - .scanner-state { - margin-bottom: 16px; - } - .state-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: 4px 0; - } - .state-value { - font-weight: 500; + ha-list-item { + --mdc-list-item-meta-display: flex; + --mdc-list-item-meta-size: 48px; } `, ]; diff --git a/src/translations/en.json b/src/translations/en.json index f9fbd03602c4..8c76b9970035 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -6013,7 +6013,7 @@ }, "bluetooth": { "title": "Bluetooth", - "settings_title": "Bluetooth settings", + "settings_title": "Bluetooth adapters", "option_flow": "Configure Bluetooth options", "advertisement_monitor": "Advertisement monitor", "advertisement_monitor_details": "The advertisement monitor listens for Bluetooth advertisements and displays the data in a structured format.", @@ -6027,6 +6027,7 @@ "no_connection_slot_allocations": "No connection slot allocations information available", "no_active_connection_support": "This adapter does not support making active (GATT) connections.", "no_scanner_state_available": "No scanner state available", + "scanner_state_unknown": "State unknown", "current_scanning_mode": "Current scanning mode", "requested_scanning_mode": "Requested scanning mode", "scanning_mode_none": "none", From 1670654ce2b7e86685f64e26bb765de7bdb66406 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Jan 2026 11:17:46 +0100 Subject: [PATCH 2/8] Move connection allocations up --- .../bluetooth/bluetooth-config-dashboard.ts | 140 ++++++++---------- src/translations/en.json | 8 +- 2 files changed, 64 insertions(+), 84 deletions(-) diff --git a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts index edf8037de077..84f969e8edc1 100644 --- a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts @@ -8,7 +8,7 @@ import "../../../../../components/ha-card"; import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-list"; import "../../../../../components/ha-list-item"; -import "../../../../../components/ha-metric"; + import type { ConfigEntry } from "../../../../../data/config_entries"; import { getConfigEntries } from "../../../../../data/config_entries"; import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow"; @@ -26,10 +26,6 @@ import type { BluetoothScannersDetails, HaScannerType, } from "../../../../../data/bluetooth"; -import { - getValueInPercentage, - roundWithOneDecimal, -} from "../../../../../util/calculate"; @customElement("bluetooth-config-dashboard") export class BluetoothConfigDashboard extends LitElement { @@ -41,16 +37,10 @@ export class BluetoothConfigDashboard extends LitElement { @state() private _connectionAllocationData: BluetoothAllocationsData[] = []; - @state() private _connectionAllocationsError?: string; - @state() private _scannerStates: Record = {}; @state() private _scannerDetails?: BluetoothScannersDetails; - private _configEntry = new URLSearchParams(window.location.search).get( - "config_entry" - ); - private _unsubConnectionAllocations?: (() => Promise) | undefined; private _unsubScannerState?: (() => Promise) | undefined; @@ -74,22 +64,16 @@ export class BluetoothConfigDashboard extends LitElement { } private async _subscribeBluetoothConnectionAllocations(): Promise { - if (this._unsubConnectionAllocations || !this._configEntry) { + if (this._unsubConnectionAllocations) { return; } - try { - this._unsubConnectionAllocations = - await subscribeBluetoothConnectionAllocations( - this.hass.connection, - (data) => { - this._connectionAllocationData = data; - }, - this._configEntry - ); - } catch (err: any) { - this._unsubConnectionAllocations = undefined; - this._connectionAllocationsError = err.message; - } + this._unsubConnectionAllocations = + await subscribeBluetoothConnectionAllocations( + this.hass.connection, + (data) => { + this._connectionAllocationData = data; + } + ); } private async _subscribeBluetoothScannerState(): Promise { @@ -137,7 +121,11 @@ export class BluetoothConfigDashboard extends LitElement { protected render(): TemplateResult { return html` - +
- ${this._renderConnectionAllocations()} +

+ ${this.hass.localize( + "ui.panel.config.bluetooth.connection_slot_allocations_monitor_description" + )} +

- roundWithOneDecimal(getValueInPercentage(used, 0, total)); - private _renderAdaptersList() { if (this._configEntries.length === 0) { return html` @@ -226,13 +215,23 @@ export class BluetoothConfigDashboard extends LitElement { scannerState && scannerState.current_mode !== scannerState.requested_mode; + // Find allocation data for this scanner + const allocations = scannerState + ? this._connectionAllocationData.find( + (a) => a.source === scannerState.source + ) + : undefined; + const secondaryText = this._formatScannerModeText(scannerState); return html` ${entry.title} - ${secondaryText} -
${secondaryText}
+ + ${secondaryText}${allocations && allocations.slots > 0 + ? ` · ${allocations.slots - allocations.free}/${allocations.slots} ${this.hass.localize("ui.panel.config.bluetooth.active_connections")}` + : nothing} + ${!isRemoteScanner ? html` ${hasMismatch - ? this._renderScannerMismatchWarning(scannerState, scannerType) + ? this._renderScannerMismatchWarning( + entry.title, + scannerState, + scannerType + ) : nothing} `; }); } private _renderScannerMismatchWarning( + name: string, scannerState: BluetoothScannerState, scannerType: HaScannerType ) { @@ -285,6 +289,7 @@ export class BluetoothConfigDashboard extends LitElement { ${this.hass.localize( "ui.panel.config.bluetooth.scanner_mode_mismatch", { + name: name, requested: this._formatMode(scannerState.requested_mode), current: this._formatMode(scannerState.current_mode), } @@ -315,6 +320,25 @@ export class BluetoothConfigDashboard extends LitElement { } } + private _formatModeLabel(mode: string | null): string { + switch (mode) { + case null: + return this.hass.localize( + "ui.panel.config.bluetooth.scanning_mode_none_label" + ); + case "active": + return this.hass.localize( + "ui.panel.config.bluetooth.scanning_mode_active_label" + ); + case "passive": + return this.hass.localize( + "ui.panel.config.bluetooth.scanning_mode_passive_label" + ); + default: + return mode; + } + } + private _formatScannerModeText( scannerState: BluetoothScannerState | undefined ): string { @@ -324,55 +348,7 @@ export class BluetoothConfigDashboard extends LitElement { ); } - const currentMode = this._formatMode(scannerState.current_mode); - - if (scannerState.current_mode === scannerState.requested_mode) { - return currentMode; - } - - return `${currentMode} (${this._formatMode(scannerState.requested_mode)})`; - } - - private _renderConnectionAllocations() { - if (this._connectionAllocationsError) { - return html`${this._connectionAllocationsError}`; - } - if (this._connectionAllocationData.length === 0) { - return html`
- ${this.hass.localize( - "ui.panel.config.bluetooth.no_connection_slot_allocations" - )} -
`; - } - const allocations = this._connectionAllocationData[0]; - const allocationsUsed = allocations.slots - allocations.free; - const allocationsTotal = allocations.slots; - if (allocationsTotal === 0) { - return html`
- ${this.hass.localize( - "ui.panel.config.bluetooth.no_active_connection_support" - )} -
`; - } - return html` -

- ${this.hass.localize( - "ui.panel.config.bluetooth.connection_slot_allocations_monitor_details", - { slots: allocationsTotal } - )} -

- 0 - ? `${allocationsUsed}/${allocationsTotal} (${allocations.allocated.join(", ")})` - : `${allocationsUsed}/${allocationsTotal}`} - > - `; + return this._formatModeLabel(scannerState.current_mode); } private _openOptionFlow(ev: Event) { diff --git a/src/translations/en.json b/src/translations/en.json index 8c76b9970035..140bf438b1ed 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -6018,11 +6018,12 @@ "advertisement_monitor": "Advertisement monitor", "advertisement_monitor_details": "The advertisement monitor listens for Bluetooth advertisements and displays the data in a structured format.", "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_description": "The connection slot allocations monitor displays the (GATT) connection slot allocations for each adapter. 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", "visualization": "Visualization", "used_connection_slot_allocations": "Used connection slot allocations", "no_connections": "No active connections", + "active_connections": "connections", "no_advertisements_found": "No matching Bluetooth advertisements found", "no_connection_slot_allocations": "No connection slot allocations information available", "no_active_connection_support": "This adapter does not support making active (GATT) connections.", @@ -6033,7 +6034,10 @@ "scanning_mode_none": "none", "scanning_mode_active": "active", "scanning_mode_passive": "passive", - "scanner_mode_mismatch": "Scanner requested {requested} mode but is operating in {current} mode. The scanner is in a bad state and needs to be power cycled.", + "scanning_mode_active_label": "Active scanning", + "scanning_mode_passive_label": "Passive scanning", + "scanning_mode_none_label": "No scanning", + "scanner_mode_mismatch": "{name} requested {requested} mode but is operating in {current} mode. The scanner is in a bad state and needs to be power cycled.", "scanner_mode_mismatch_remote": "For proxies: reboot the device", "scanner_mode_mismatch_usb": "For USB adapters: unplug and plug back in", "scanner_mode_mismatch_uart": "For UART/onboard adapters: power down the system completely and power it back up", From d1b19d5c3eadea09a8829cf38d81c2c43b23f254 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Jan 2026 11:28:18 +0100 Subject: [PATCH 3/8] Make it tabs --- .../bluetooth-advertisement-monitor.ts | 15 +--- .../bluetooth/bluetooth-config-dashboard.ts | 89 +++++++------------ .../bluetooth/bluetooth-connection-monitor.ts | 2 + .../bluetooth-network-visualization.ts | 7 +- src/translations/en.json | 19 ++-- 5 files changed, 44 insertions(+), 88 deletions(-) diff --git a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-advertisement-monitor.ts b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-advertisement-monitor.ts index 28bc0b8ed88b..69adbbee73e7 100644 --- a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-advertisement-monitor.ts +++ b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-advertisement-monitor.ts @@ -23,23 +23,12 @@ import { subscribeBluetoothScannersDetails, } from "../../../../../data/bluetooth"; import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry"; -import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage"; import "../../../../../layouts/hass-tabs-subpage-data-table"; import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant, Route } from "../../../../../types"; +import { bluetoothTabs } from "./bluetooth-config-dashboard"; import { showBluetoothDeviceInfoDialog } from "./show-dialog-bluetooth-device-info"; -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") export class BluetoothAdvertisementMonitorPanel extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -232,7 +221,7 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement { @collapsed-changed=${this._handleCollapseChanged} filter=${this.address || ""} clickable - .tabs=${bluetoothAdvertisementMonitorTabs} + .tabs=${bluetoothTabs} > `; } diff --git a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts index 84f969e8edc1..ed883fd6cbeb 100644 --- a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts @@ -12,9 +12,10 @@ import "../../../../../components/ha-list-item"; import type { ConfigEntry } from "../../../../../data/config_entries"; import { getConfigEntries } from "../../../../../data/config_entries"; import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow"; -import "../../../../../layouts/hass-subpage"; +import "../../../../../layouts/hass-tabs-subpage"; +import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage"; import { haStyle } from "../../../../../resources/styles"; -import type { HomeAssistant } from "../../../../../types"; +import type { HomeAssistant, Route } from "../../../../../types"; import { subscribeBluetoothConnectionAllocations, subscribeBluetoothScannerState, @@ -27,12 +28,35 @@ import type { HaScannerType, } from "../../../../../data/bluetooth"; +export const bluetoothTabs: PageNavigation[] = [ + { + translationKey: "ui.panel.config.bluetooth.tabs.overview", + path: `/config/bluetooth/dashboard`, + }, + { + translationKey: "ui.panel.config.bluetooth.tabs.advertisements", + path: `/config/bluetooth/advertisement-monitor`, + }, + { + translationKey: "ui.panel.config.bluetooth.tabs.visualization", + path: `/config/bluetooth/visualization`, + }, + { + translationKey: "ui.panel.config.bluetooth.tabs.connections", + path: `/config/bluetooth/connection-monitor`, + }, +]; + @customElement("bluetooth-config-dashboard") export class BluetoothConfigDashboard extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public route!: Route; + @property({ type: Boolean }) public narrow = false; + @property({ attribute: "is-wide", type: Boolean }) public isWide = false; + @state() private _configEntries: ConfigEntry[] = []; @state() private _connectionAllocationData: BluetoothAllocationsData[] = []; @@ -121,10 +145,11 @@ export class BluetoothConfigDashboard extends LitElement { protected render(): TemplateResult { return html` -
${this._renderAdaptersList()} - -
-

- ${this.hass.localize( - "ui.panel.config.bluetooth.advertisement_monitor_details" - )} -

-
-
- - ${this.hass.localize( - "ui.panel.config.bluetooth.advertisement_monitor" - )} - - - ${this.hass.localize("ui.panel.config.bluetooth.visualization")} - -
-
- -
-

- ${this.hass.localize( - "ui.panel.config.bluetooth.connection_slot_allocations_monitor_description" - )} -

-
-
- - ${this.hass.localize( - "ui.panel.config.bluetooth.connection_monitor" - )} - -
-
-
+ `; } diff --git a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-connection-monitor.ts b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-connection-monitor.ts index cd0f64a1cd0c..8ccea8ffdb82 100644 --- a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-connection-monitor.ts +++ b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-connection-monitor.ts @@ -24,6 +24,7 @@ import type { DeviceRegistryEntry } from "../../../../../data/device/device_regi import "../../../../../layouts/hass-tabs-subpage-data-table"; import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant, Route } from "../../../../../types"; +import { bluetoothTabs } from "./bluetooth-config-dashboard"; @customElement("bluetooth-connection-monitor") export class BluetoothConnectionMonitorPanel extends LitElement { @@ -214,6 +215,7 @@ export class BluetoothConnectionMonitorPanel extends LitElement { .hass=${this.hass} .narrow=${this.narrow} .route=${this.route} + .tabs=${bluetoothTabs} .columns=${this._columns(this.hass.localize)} .data=${this._dataWithNamedSourceAndIds(this._data)} .initialGroupColumn=${this._activeGrouping} diff --git a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-network-visualization.ts b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-network-visualization.ts index 7e629c4f1387..770c1468f032 100644 --- a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-network-visualization.ts +++ b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-network-visualization.ts @@ -26,9 +26,9 @@ import { subscribeBluetoothScannersDetails, } from "../../../../../data/bluetooth"; import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry"; -import "../../../../../layouts/hass-subpage"; +import "../../../../../layouts/hass-tabs-subpage"; import type { HomeAssistant, Route } from "../../../../../types"; -import { bluetoothAdvertisementMonitorTabs } from "./bluetooth-advertisement-monitor"; +import { bluetoothTabs } from "./bluetooth-config-dashboard"; const UPDATE_THROTTLE_TIME = 10000; @@ -123,8 +123,7 @@ export class BluetoothNetworkVisualization extends LitElement { .hass=${this.hass} .narrow=${this.narrow} .route=${this.route} - header=${this.hass.localize("ui.panel.config.bluetooth.visualization")} - .tabs=${bluetoothAdvertisementMonitorTabs} + .tabs=${bluetoothTabs} > Date: Mon, 5 Jan 2026 10:49:12 +0100 Subject: [PATCH 4/8] Add icons --- .../bluetooth/bluetooth-config-dashboard.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts index ed883fd6cbeb..2d3f39b139d2 100644 --- a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts @@ -1,4 +1,10 @@ -import { mdiCogOutline } from "@mdi/js"; +import { + mdiBroadcast, + mdiCogOutline, + mdiLan, + mdiLinkVariant, + mdiNetwork, +} from "@mdi/js"; import type { CSSResultGroup, TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -32,18 +38,22 @@ export const bluetoothTabs: PageNavigation[] = [ { translationKey: "ui.panel.config.bluetooth.tabs.overview", path: `/config/bluetooth/dashboard`, + iconPath: mdiNetwork, }, { translationKey: "ui.panel.config.bluetooth.tabs.advertisements", path: `/config/bluetooth/advertisement-monitor`, + iconPath: mdiBroadcast, }, { translationKey: "ui.panel.config.bluetooth.tabs.visualization", path: `/config/bluetooth/visualization`, + iconPath: mdiLan, }, { translationKey: "ui.panel.config.bluetooth.tabs.connections", path: `/config/bluetooth/connection-monitor`, + iconPath: mdiLinkVariant, }, ]; From f439c1a6bcce63ad737041a09b2fca77a7d7d88d Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 6 Jan 2026 09:33:19 +0100 Subject: [PATCH 5/8] Revert "Add icons" This reverts commit e338b6e5788ca47a3bf98e1cd5fc7ccc46605224. --- .../bluetooth/bluetooth-config-dashboard.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts index 2d3f39b139d2..ed883fd6cbeb 100644 --- a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts @@ -1,10 +1,4 @@ -import { - mdiBroadcast, - mdiCogOutline, - mdiLan, - mdiLinkVariant, - mdiNetwork, -} from "@mdi/js"; +import { mdiCogOutline } from "@mdi/js"; import type { CSSResultGroup, TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -38,22 +32,18 @@ export const bluetoothTabs: PageNavigation[] = [ { translationKey: "ui.panel.config.bluetooth.tabs.overview", path: `/config/bluetooth/dashboard`, - iconPath: mdiNetwork, }, { translationKey: "ui.panel.config.bluetooth.tabs.advertisements", path: `/config/bluetooth/advertisement-monitor`, - iconPath: mdiBroadcast, }, { translationKey: "ui.panel.config.bluetooth.tabs.visualization", path: `/config/bluetooth/visualization`, - iconPath: mdiLan, }, { translationKey: "ui.panel.config.bluetooth.tabs.connections", path: `/config/bluetooth/connection-monitor`, - iconPath: mdiLinkVariant, }, ]; From 968d79d811b2dc535fceb02ce0dc095fe26f7d47 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 6 Jan 2026 09:33:29 +0100 Subject: [PATCH 6/8] Revert "Make it tabs" This reverts commit d1b19d5c3eadea09a8829cf38d81c2c43b23f254. --- .../bluetooth-advertisement-monitor.ts | 15 +++- .../bluetooth/bluetooth-config-dashboard.ts | 89 ++++++++++++------- .../bluetooth/bluetooth-connection-monitor.ts | 2 - .../bluetooth-network-visualization.ts | 7 +- src/translations/en.json | 19 ++-- 5 files changed, 88 insertions(+), 44 deletions(-) diff --git a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-advertisement-monitor.ts b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-advertisement-monitor.ts index 69adbbee73e7..28bc0b8ed88b 100644 --- a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-advertisement-monitor.ts +++ b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-advertisement-monitor.ts @@ -23,12 +23,23 @@ import { subscribeBluetoothScannersDetails, } from "../../../../../data/bluetooth"; import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry"; +import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage"; import "../../../../../layouts/hass-tabs-subpage-data-table"; import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant, Route } from "../../../../../types"; -import { bluetoothTabs } from "./bluetooth-config-dashboard"; import { showBluetoothDeviceInfoDialog } from "./show-dialog-bluetooth-device-info"; +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") export class BluetoothAdvertisementMonitorPanel extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -221,7 +232,7 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement { @collapsed-changed=${this._handleCollapseChanged} filter=${this.address || ""} clickable - .tabs=${bluetoothTabs} + .tabs=${bluetoothAdvertisementMonitorTabs} > `; } diff --git a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts index ed883fd6cbeb..84f969e8edc1 100644 --- a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts @@ -12,10 +12,9 @@ import "../../../../../components/ha-list-item"; import type { ConfigEntry } from "../../../../../data/config_entries"; import { getConfigEntries } from "../../../../../data/config_entries"; import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow"; -import "../../../../../layouts/hass-tabs-subpage"; -import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage"; +import "../../../../../layouts/hass-subpage"; import { haStyle } from "../../../../../resources/styles"; -import type { HomeAssistant, Route } from "../../../../../types"; +import type { HomeAssistant } from "../../../../../types"; import { subscribeBluetoothConnectionAllocations, subscribeBluetoothScannerState, @@ -28,35 +27,12 @@ import type { HaScannerType, } from "../../../../../data/bluetooth"; -export const bluetoothTabs: PageNavigation[] = [ - { - translationKey: "ui.panel.config.bluetooth.tabs.overview", - path: `/config/bluetooth/dashboard`, - }, - { - translationKey: "ui.panel.config.bluetooth.tabs.advertisements", - path: `/config/bluetooth/advertisement-monitor`, - }, - { - translationKey: "ui.panel.config.bluetooth.tabs.visualization", - path: `/config/bluetooth/visualization`, - }, - { - translationKey: "ui.panel.config.bluetooth.tabs.connections", - path: `/config/bluetooth/connection-monitor`, - }, -]; - @customElement("bluetooth-config-dashboard") export class BluetoothConfigDashboard extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public route!: Route; - @property({ type: Boolean }) public narrow = false; - @property({ attribute: "is-wide", type: Boolean }) public isWide = false; - @state() private _configEntries: ConfigEntry[] = []; @state() private _connectionAllocationData: BluetoothAllocationsData[] = []; @@ -145,11 +121,10 @@ export class BluetoothConfigDashboard extends LitElement { protected render(): TemplateResult { return html` -
${this._renderAdaptersList()} + +
+

+ ${this.hass.localize( + "ui.panel.config.bluetooth.advertisement_monitor_details" + )} +

+
+
+ + ${this.hass.localize( + "ui.panel.config.bluetooth.advertisement_monitor" + )} + + + ${this.hass.localize("ui.panel.config.bluetooth.visualization")} + +
+
+ +
+

+ ${this.hass.localize( + "ui.panel.config.bluetooth.connection_slot_allocations_monitor_description" + )} +

+
+
+ + ${this.hass.localize( + "ui.panel.config.bluetooth.connection_monitor" + )} + +
+
-
+ `; } diff --git a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-connection-monitor.ts b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-connection-monitor.ts index 8ccea8ffdb82..cd0f64a1cd0c 100644 --- a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-connection-monitor.ts +++ b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-connection-monitor.ts @@ -24,7 +24,6 @@ import type { DeviceRegistryEntry } from "../../../../../data/device/device_regi import "../../../../../layouts/hass-tabs-subpage-data-table"; import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant, Route } from "../../../../../types"; -import { bluetoothTabs } from "./bluetooth-config-dashboard"; @customElement("bluetooth-connection-monitor") export class BluetoothConnectionMonitorPanel extends LitElement { @@ -215,7 +214,6 @@ export class BluetoothConnectionMonitorPanel extends LitElement { .hass=${this.hass} .narrow=${this.narrow} .route=${this.route} - .tabs=${bluetoothTabs} .columns=${this._columns(this.hass.localize)} .data=${this._dataWithNamedSourceAndIds(this._data)} .initialGroupColumn=${this._activeGrouping} diff --git a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-network-visualization.ts b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-network-visualization.ts index 770c1468f032..7e629c4f1387 100644 --- a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-network-visualization.ts +++ b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-network-visualization.ts @@ -26,9 +26,9 @@ import { subscribeBluetoothScannersDetails, } from "../../../../../data/bluetooth"; import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry"; -import "../../../../../layouts/hass-tabs-subpage"; +import "../../../../../layouts/hass-subpage"; import type { HomeAssistant, Route } from "../../../../../types"; -import { bluetoothTabs } from "./bluetooth-config-dashboard"; +import { bluetoothAdvertisementMonitorTabs } from "./bluetooth-advertisement-monitor"; const UPDATE_THROTTLE_TIME = 10000; @@ -123,7 +123,8 @@ export class BluetoothNetworkVisualization extends LitElement { .hass=${this.hass} .narrow=${this.narrow} .route=${this.route} - .tabs=${bluetoothTabs} + header=${this.hass.localize("ui.panel.config.bluetooth.visualization")} + .tabs=${bluetoothAdvertisementMonitorTabs} > Date: Tue, 6 Jan 2026 09:58:25 +0100 Subject: [PATCH 7/8] Fix scanner matching and no active connection slot support --- .../bluetooth/bluetooth-config-dashboard.ts | 63 ++++++++++++------- src/translations/en.json | 2 +- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts index 84f969e8edc1..5867e527e1c9 100644 --- a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts @@ -1,6 +1,6 @@ import { mdiCogOutline } from "@mdi/js"; import type { CSSResultGroup, TemplateResult } from "lit"; -import { css, html, LitElement, nothing } from "lit"; +import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import "../../../../../components/ha-alert"; import "../../../../../components/ha-button"; @@ -9,23 +9,24 @@ import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-list"; import "../../../../../components/ha-list-item"; -import type { ConfigEntry } from "../../../../../data/config_entries"; -import { getConfigEntries } from "../../../../../data/config_entries"; -import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow"; -import "../../../../../layouts/hass-subpage"; -import { haStyle } from "../../../../../resources/styles"; -import type { HomeAssistant } from "../../../../../types"; -import { - subscribeBluetoothConnectionAllocations, - subscribeBluetoothScannerState, - subscribeBluetoothScannersDetails, -} from "../../../../../data/bluetooth"; import type { BluetoothAllocationsData, BluetoothScannerState, BluetoothScannersDetails, HaScannerType, } from "../../../../../data/bluetooth"; +import { + subscribeBluetoothConnectionAllocations, + subscribeBluetoothScannerState, + subscribeBluetoothScannersDetails, +} from "../../../../../data/bluetooth"; +import type { ConfigEntry } from "../../../../../data/config_entries"; +import { getConfigEntries } from "../../../../../data/config_entries"; +import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry"; +import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow"; +import "../../../../../layouts/hass-subpage"; +import { haStyle } from "../../../../../resources/styles"; +import type { HomeAssistant } from "../../../../../types"; @customElement("bluetooth-config-dashboard") export class BluetoothConfigDashboard extends LitElement { @@ -200,13 +201,27 @@ export class BluetoothConfigDashboard extends LitElement {
`; } - return this._configEntries.map((entry) => { - // Find scanner state by matching scanner details name to config entry title - const scannerState = Object.values(this._scannerStates).find( - (s) => this._scannerDetails?.[s.source]?.name === entry.title + // Build source to device mapping (same as visualization) + const sourceDevices: Record = {}; + Object.values(this.hass.devices).forEach((device) => { + const btConnection = device.connections.find( + (connection) => connection[0] === "bluetooth" ); - const scannerDetails = scannerState - ? this._scannerDetails?.[scannerState.source] + if (btConnection) { + sourceDevices[btConnection[1]] = device; + } + }); + + return this._configEntries.map((entry) => { + // Find scanner by matching device's config_entries to this entry + const scannerDetails = this._scannerDetails + ? Object.values(this._scannerDetails).find((d) => { + const device = sourceDevices[d.source]; + return device?.config_entries.includes(entry.entry_id); + }) + : undefined; + const scannerState = scannerDetails + ? this._scannerStates[scannerDetails.source] : undefined; const scannerType: HaScannerType = scannerDetails?.scanner_type ?? "unknown"; @@ -216,9 +231,9 @@ export class BluetoothConfigDashboard extends LitElement { scannerState.current_mode !== scannerState.requested_mode; // Find allocation data for this scanner - const allocations = scannerState + const allocations = scannerDetails ? this._connectionAllocationData.find( - (a) => a.source === scannerState.source + (a) => a.source === scannerDetails.source ) : undefined; @@ -228,8 +243,10 @@ export class BluetoothConfigDashboard extends LitElement { ${entry.title} - ${secondaryText}${allocations && allocations.slots > 0 - ? ` · ${allocations.slots - allocations.free}/${allocations.slots} ${this.hass.localize("ui.panel.config.bluetooth.active_connections")}` + ${secondaryText}${allocations + ? allocations.slots > 0 + ? ` · ${allocations.slots - allocations.free}/${allocations.slots} ${this.hass.localize("ui.panel.config.bluetooth.active_connections")}` + : ` · ${this.hass.localize("ui.panel.config.bluetooth.no_connection_slots")}` : nothing} ${!isRemoteScanner @@ -244,7 +261,7 @@ export class BluetoothConfigDashboard extends LitElement { >` : nothing} - ${hasMismatch + ${hasMismatch && scannerDetails ? this._renderScannerMismatchWarning( entry.title, scannerState, diff --git a/src/translations/en.json b/src/translations/en.json index 10bd7f53400d..1718e3a1a850 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -6027,7 +6027,7 @@ "active_connections": "connections", "no_advertisements_found": "No matching Bluetooth advertisements found", "no_connection_slot_allocations": "No connection slot allocations information available", - "no_active_connection_support": "This adapter does not support making active (GATT) connections.", + "no_connection_slots": "No connection slots", "no_scanner_state_available": "No scanner state available", "scanner_state_unknown": "State unknown", "current_scanning_mode": "Current scanning mode", From a0a53b134881a8e2696cd6a6a727e8150bad3f7f Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 6 Jan 2026 15:31:08 +0200 Subject: [PATCH 8/8] Update src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts --- .../integration-panels/bluetooth/bluetooth-config-dashboard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts index 5867e527e1c9..246b6ff20207 100644 --- a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts @@ -123,7 +123,7 @@ export class BluetoothConfigDashboard extends LitElement { protected render(): TemplateResult { return html`