Add support for showing Bluetooth connection slot allocations (#23899)

This commit is contained in:
J. Nick Koston 2025-01-28 06:52:21 -10:00 committed by GitHub
parent f8d2560104
commit cbdb7406ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 143 additions and 10 deletions

View File

@ -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<BluetoothDeviceData[]>
): Promise<UnsubscribeFunc> =>
@ -78,13 +85,32 @@ const subscribeUpdates = (
export const subscribeBluetoothAdvertisements = (
conn: Connection,
onChange: (bluetoothDeviceData: BluetoothDeviceData[]) => void
callbackFunction: (bluetoothDeviceData: BluetoothDeviceData[]) => void
) =>
createCollection<BluetoothDeviceData[]>(
"_bluetoothDeviceRows",
() => Promise.resolve<BluetoothDeviceData[]>([]), // empty array as initial state
subscribeUpdates,
subscribeBluetoothAdvertisementsUpdates,
conn,
onChange
callbackFunction
);
export const subscribeBluetoothConnectionAllocations = (
conn: Connection,
callbackFunction: (
bluetoothAllocationsData: BluetoothAllocationsData[]
) => void,
configEntryId?: string
): Promise<() => Promise<void>> => {
const params: { type: string; config_entry_id?: string } = {
type: "bluetooth/subscribe_connection_allocations",
};
if (configEntryId) {
params.config_entry_id = configEntryId;
}
return conn.subscribeMessage<BluetoothAllocationsData[]>(
(bluetoothAllocationsData) => callbackFunction(bluetoothAllocationsData),
params
);
};

View File

@ -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<void>) | undefined;
public connectedCallback(): void {
super.connectedCallback();
if (this.hass) {
this._subscribeBluetoothConnectionAllocations();
}
}
private async _subscribeBluetoothConnectionAllocations(): Promise<void> {
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`
<hass-subpage .narrow=${this.narrow} .hass=${this.hass}>
@ -57,17 +108,68 @@ export class BluetoothConfigDashboard extends LitElement {
>
</div>
</ha-card>
<ha-card
.header=${this.hass.localize(
"ui.panel.config.bluetooth.connection_slot_allocations_monitor"
)}
>
<div class="card-content">
${this._renderConnectionAllocations()}
</div>
</ha-card>
</div>
</hass-subpage>
`;
}
private _getUsedAllocations = (used: number, total: number) =>
roundWithOneDecimal(getValueInPercentage(used, 0, total));
private _renderConnectionAllocations() {
if (this._connectionAllocationsError) {
return html`<ha-alert alert-type="error"
>${this._connectionAllocationsError}</ha-alert
>`;
}
if (this._connectionAllocationData.length === 0) {
return html`<div>
${this.hass.localize(
"ui.panel.config.bluetooth.no_connection_slot_allocations"
)}
</div>`;
}
const allocations = this._connectionAllocationData[0];
const allocationsUsed = allocations.slots - allocations.free;
const allocationsTotal = allocations.slots;
if (allocationsTotal === 0) {
return html`<div>
${this.hass.localize(
"ui.panel.config.bluetooth.no_active_connection_support"
)}
</div>`;
}
return html`
<p>
${this.hass.localize(
"ui.panel.config.bluetooth.connection_slot_allocations_monitor_details",
{ slots: allocationsTotal }
)}
</p>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.bluetooth.used_connection_slot_allocations"
)}
.value=${this._getUsedAllocations(allocationsUsed, allocationsTotal)}
.tooltip=${`${allocationsUsed}/${allocationsTotal}`}
></ha-metric>
`;
}
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;
}
`,

View File

@ -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",