diff --git a/src/data/bluetooth.ts b/src/data/bluetooth.ts index a9b2f082dd..49d11999d1 100644 --- a/src/data/bluetooth.ts +++ b/src/data/bluetooth.ts @@ -29,7 +29,14 @@ interface BluetoothAdvertisementSubscriptionMessage { remove?: BluetoothRemoveDeviceData[]; } -const subscribeUpdates = ( +export interface BluetoothAllocationsData { + source: string; + slots: number; + free: number; + allocated: string[]; +} + +const subscribeBluetoothAdvertisementsUpdates = ( conn: Connection, store: Store ): Promise => @@ -78,13 +85,32 @@ const subscribeUpdates = ( export const subscribeBluetoothAdvertisements = ( conn: Connection, - onChange: (bluetoothDeviceData: BluetoothDeviceData[]) => void + callbackFunction: (bluetoothDeviceData: BluetoothDeviceData[]) => void ) => createCollection( "_bluetoothDeviceRows", () => Promise.resolve([]), // empty array as initial state - subscribeUpdates, + subscribeBluetoothAdvertisementsUpdates, conn, - onChange + callbackFunction ); + +export const subscribeBluetoothConnectionAllocations = ( + conn: Connection, + callbackFunction: ( + bluetoothAllocationsData: BluetoothAllocationsData[] + ) => void, + configEntryId?: string +): Promise<() => Promise> => { + const params: { type: string; config_entry_id?: string } = { + type: "bluetooth/subscribe_connection_allocations", + }; + if (configEntryId) { + params.config_entry_id = configEntryId; + } + return conn.subscribeMessage( + (bluetoothAllocationsData) => callbackFunction(bluetoothAllocationsData), + params + ); +}; 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 17a97e7cd8..a09be536b4 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,7 +1,7 @@ import "@material/mwc-button"; import type { CSSResultGroup, TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import "../../../../../components/ha-card"; import "../../../../../components/ha-code-editor"; import "../../../../../components/ha-formfield"; @@ -11,6 +11,13 @@ import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-d import "../../../../../layouts/hass-subpage"; import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant } from "../../../../../types"; +import { subscribeBluetoothConnectionAllocations } from "../../../../../data/bluetooth"; +import { + getValueInPercentage, + roundWithOneDecimal, +} from "../../../../../util/calculate"; +import "../../../../../components/ha-metric"; +import type { BluetoothAllocationsData } from "../../../../../data/bluetooth"; @customElement("bluetooth-config-dashboard") export class BluetoothConfigDashboard extends LitElement { @@ -18,6 +25,50 @@ export class BluetoothConfigDashboard extends LitElement { @property({ type: Boolean }) public narrow = false; + @state() private _connectionAllocationData: BluetoothAllocationsData[] = []; + + @state() private _connectionAllocationsError?: string; + + private _configEntry = new URLSearchParams(window.location.search).get( + "config_entry" + ); + + private _unsubConnectionAllocations?: (() => Promise) | undefined; + + public connectedCallback(): void { + super.connectedCallback(); + if (this.hass) { + this._subscribeBluetoothConnectionAllocations(); + } + } + + private async _subscribeBluetoothConnectionAllocations(): Promise { + if (this._unsubConnectionAllocations || !this._configEntry) { + 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; + } + } + + public disconnectedCallback() { + super.disconnectedCallback(); + if (this._unsubConnectionAllocations) { + this._unsubConnectionAllocations(); + this._unsubConnectionAllocations = undefined; + } + } + protected render(): TemplateResult { return html` @@ -57,17 +108,68 @@ export class BluetoothConfigDashboard extends LitElement { > + +
+ ${this._renderConnectionAllocations()} +
+
`; } + private _getUsedAllocations = (used: number, total: number) => + roundWithOneDecimal(getValueInPercentage(used, 0, total)); + + 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 } + )} +

+ + `; + } + private async _openOptionFlow() { - const searchParams = new URLSearchParams(window.location.search); - if (!searchParams.has("config_entry")) { + const configEntryId = this._configEntry; + if (!configEntryId) { return; } - const configEntryId = searchParams.get("config_entry") as string; const configEntries = await getConfigEntries(this.hass, { domain: "bluetooth", }); @@ -92,7 +194,7 @@ export class BluetoothConfigDashboard extends LitElement { margin: 0 auto; direction: ltr; } - ha-card:first-child { + ha-card { margin-bottom: 16px; } `, diff --git a/src/translations/en.json b/src/translations/en.json index 93a3307736..0816ebbb29 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5293,8 +5293,13 @@ "title": "Bluetooth", "settings_title": "Bluetooth settings", "option_flow": "Configure Bluetooth options", - "advertisement_monitor": "Advertisement Monitor", + "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.", + "used_connection_slot_allocations": "Used connection slot allocations", + "no_connection_slot_allocations": "No connection slot allocations information available", + "no_active_connection_support": "This adapter does not support making active (GATT) connections.", "address": "Address", "name": "Name", "source": "Source",