Compare commits

..

9 Commits

Author SHA1 Message Date
Paulus Schoutsen
d1b19d5c3e Make it tabs 2026-01-02 11:28:18 +01:00
Paulus Schoutsen
1670654ce2 Move connection allocations up 2026-01-02 11:17:46 +01:00
Paulus Schoutsen
94cddc91ff Support multiple adapters in bluetooth panel 2026-01-01 14:19:42 +01:00
Paulus Schoutsen
8d317d1e2c Hide dashboard controls in kiosk mode (#28742) 2026-01-01 00:36:49 +01:00
Simon Lamon
9acad2e83c Provide kioskmode in demo (#28739) 2025-12-30 22:45:01 +01:00
ildar170975
9099c5a92c Map card editor: add a basic sub-element editor (#28687)
* add subelement editor

* explicit type convertion

* test

* test

* test

* test

* prettier
2025-12-30 20:18:57 +01:00
Paulus Schoutsen
60c4d60d66 Protocol link updates (#28736)
* Update icons Thread & Insteon

* Remove matter link

* Remove back path from ZHA

* Fix ZHA dashboard config entry
2025-12-30 19:54:48 +01:00
sebcaps
e8a4cde643 Add energy percentage usage on pie chart view. (#28733)
* showPercent

* unnecessary change
2025-12-30 19:54:35 +01:00
renovate[bot]
148eab31b6 Update dependency jsdom to v27.4.0 (#28726)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-29 19:18:29 +01:00
16 changed files with 341 additions and 361 deletions

View File

@@ -199,7 +199,7 @@
"gulp-rename": "2.1.0",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "27.3.0",
"jsdom": "27.4.0",
"jszip": "3.10.1",
"lint-staged": "16.2.7",
"lit-analyzer": "2.0.3",

View File

@@ -44,6 +44,7 @@ class HaNavigationList extends LitElement {
>
<ha-svg-icon
.path=${page.iconPath}
.secondaryPath=${page.iconSecondaryPath}
.viewBox=${page.iconViewBox}
></ha-svg-icon>
</div>

View File

@@ -282,6 +282,7 @@ export const provideHass = (
dockedSidebar: "auto",
vibrate: true,
debugConnection: false,
kioskMode: false,
suspendWhenHidden: false,
moreInfoEntityId: null as any,
// @ts-ignore

View File

@@ -23,6 +23,7 @@ export interface PageNavigation {
core?: boolean;
advancedOnly?: boolean;
iconPath?: string;
iconSecondaryPath?: string;
iconViewBox?: string;
description?: string;
iconColor?: string;

View File

@@ -107,15 +107,6 @@ export const configSections: Record<string, PageNavigation[]> = {
},
],
dashboard_2: [
{
path: "/config/matter",
name: "Matter",
iconPath:
"M7.228375 6.41685c0.98855 0.80195 2.16365 1.3412 3.416275 1.56765V1.30093l1.3612 -0.7854275 1.360125 0.7854275V7.9845c1.252875 -0.226675 2.4283 -0.765875 3.41735 -1.56765l2.471225 1.4293c-4.019075 3.976275 -10.490025 3.976275 -14.5091 0l2.482925 -1.4293Zm3.00335 17.067575c1.43325 -5.47035 -1.8052 -11.074775 -7.2604 -12.564675v2.859675c1.189125 0.455 2.244125 1.202875 3.0672 2.174275L0.25 19.2955v1.5719l1.3611925 0.781175L7.39865 18.3068c0.430175 1.19825 0.550625 2.48575 0.35015 3.743l2.482925 1.434625ZM21.034 10.91975c-5.452225 1.4932 -8.6871 7.09635 -7.254025 12.564675l2.47655 -1.43035c-0.200025 -1.257275 -0.079575 -2.544675 0.35015 -3.743025l5.7832 3.337525L23.75 20.86315V19.2955L17.961475 15.9537c0.8233 -0.97115 1.878225 -1.718975 3.0672 -2.174275l0.005325 -2.859675Z",
iconColor: "#2458B3",
component: "matter",
translationKey: "matter",
},
{
path: "/config/zha",
name: "Zigbee",
@@ -145,6 +136,8 @@ export const configSections: Record<string, PageNavigation[]> = {
name: "Thread",
iconPath:
"M82.498,0C37.008,0,0,37.008,0,82.496c0,45.181,36.51,81.977,81.573,82.476V82.569l-27.002-0.002 c-8.023,0-14.554,6.53-14.554,14.561c0,8.023,6.531,14.551,14.554,14.551v17.98c-17.939,0-32.534-14.595-32.534-32.531 c0-17.944,14.595-32.543,32.534-32.543l27.002,0.004v-9.096c0-14.932,12.146-27.08,27.075-27.08 c14.932,0,27.082,12.148,27.082,27.08c0,14.931-12.15,27.08-27.082,27.08l-9.097-0.001v80.641 C136.889,155.333,165,122.14,165,82.496C165,37.008,127.99,0,82.498,0z",
iconSecondaryPath:
"M117.748 55.493C117.748 50.477 113.666 46.395 108.648 46.395C103.633 46.395 99.551 50.477 99.551 55.493V64.59L108.648 64.591C113.666 64.591 117.748 60.51 117.748 55.493Z",
iconViewBox: "0 0 165 165",
iconColor: "#ED7744",
component: "thread",
@@ -161,7 +154,9 @@ export const configSections: Record<string, PageNavigation[]> = {
{
path: "/insteon",
name: "Insteon",
iconPath: mdiLan,
iconPath:
"M82.5108 43.8917H82.7152C107.824 43.8917 129.241 28.1166 137.629 5.95738L105.802 0L82.5108 43.8917ZM82.5108 43.8917H82.3065C57.1975 43.8917 35.7811 28.1352 27.3928 5.95738H27.3742L59.2015 0L82.5108 43.8917ZM43.8903 82.4951V82.2908C43.8903 57.1805 28.1158 35.7636 5.95718 27.3751L0 59.2037L43.8903 82.4951ZM43.8903 82.4951V82.6989C43.8903 107.809 28.1343 129.226 5.95718 137.615V137.633L0 105.805L43.8903 82.4951ZM165 59.2037L159.043 27.3751V27.3936C136.865 35.7822 121.11 57.1991 121.11 82.3094V82.5133V82.7176V82.7363C121.11 107.846 136.884 129.263 159.043 137.652L165 105.823L121.11 82.5133L165 59.2037ZM137.628 159.043L105.8 165L82.4912 121.108H82.695C107.804 121.108 129.221 136.865 137.609 159.043H137.628ZM82.4912 121.108L59.1818 165L27.3545 159.043C35.7428 136.884 57.1592 121.108 82.2682 121.108H82.2868H82.4912Z",
iconViewBox: "0 0 165 165",
iconColor: "#E4002C",
component: "insteon",
translationKey: "insteon",

View File

@@ -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}
></hass-tabs-subpage-data-table>
`;
}

View File

@@ -1,16 +1,21 @@
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 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,
@@ -22,30 +27,44 @@ import type {
BluetoothScannersDetails,
HaScannerType,
} from "../../../../../data/bluetooth";
import {
getValueInPercentage,
roundWithOneDecimal,
} from "../../../../../util/calculate";
import "../../../../../components/ha-metric";
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[] = [];
@state() private _connectionAllocationsError?: string;
@state() private _scannerState?: BluetoothScannerState;
@state() private _scannerStates: Record<string, BluetoothScannerState> = {};
@state() private _scannerDetails?: BluetoothScannersDetails;
private _configEntry = new URLSearchParams(window.location.search).get(
"config_entry"
);
private _unsubConnectionAllocations?: (() => Promise<void>) | undefined;
private _unsubScannerState?: (() => Promise<void>) | undefined;
@@ -55,41 +74,44 @@ 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<void> {
this._configEntries = await getConfigEntries(this.hass, {
domain: "bluetooth",
});
}
private async _subscribeBluetoothConnectionAllocations(): Promise<void> {
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<void> {
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,92 +144,94 @@ 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`
<hass-subpage .narrow=${this.narrow} .hass=${this.hass}>
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${bluetoothTabs}
>
<div class="content">
<ha-card
.header=${this.hass.localize(
"ui.panel.config.bluetooth.settings_title"
)}
>
<div class="card-content">${this._renderScannerState()}</div>
${!isRemoteScanner
? html`<div class="card-actions">
<ha-button @click=${this._openOptionFlow}
>${this.hass.localize(
"ui.panel.config.bluetooth.option_flow"
)}</ha-button
>
</div>`
: nothing}
</ha-card>
<ha-card
.header=${this.hass.localize(
"ui.panel.config.bluetooth.advertisement_monitor"
)}
>
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.bluetooth.advertisement_monitor_details"
)}
</p>
</div>
<div class="card-actions">
<ha-button
href="/config/bluetooth/advertisement-monitor"
appearance="plain"
>
${this.hass.localize(
"ui.panel.config.bluetooth.advertisement_monitor"
)}
</ha-button>
<ha-button
href="/config/bluetooth/visualization"
appearance="plain"
>
${this.hass.localize("ui.panel.config.bluetooth.visualization")}
</ha-button>
</div>
</ha-card>
<ha-card
.header=${this.hass.localize(
"ui.panel.config.bluetooth.connection_slot_allocations_monitor"
)}
>
<div class="card-content">
${this._renderConnectionAllocations()}
</div>
<div class="card-actions">
<ha-button
href="/config/bluetooth/connection-monitor"
appearance="plain"
>
${this.hass.localize(
"ui.panel.config.bluetooth.connection_monitor"
)}
</ha-button>
</div>
<ha-list>${this._renderAdaptersList()}</ha-list>
</ha-card>
</div>
</hass-subpage>
</hass-tabs-subpage>
`;
}
private _getUsedAllocations = (used: number, total: number) =>
roundWithOneDecimal(getValueInPercentage(used, 0, total));
private _renderAdaptersList() {
if (this._configEntries.length === 0) {
return html`<ha-list-item noninteractive>
${this.hass.localize(
"ui.panel.config.bluetooth.no_scanner_state_available"
)}
</ha-list-item>`;
}
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;
// 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`
<ha-list-item twoline hasMeta noninteractive>
<span>${entry.title}</span>
<span slot="secondary">
${secondaryText}${allocations && allocations.slots > 0
? ` · ${allocations.slots - allocations.free}/${allocations.slots} ${this.hass.localize("ui.panel.config.bluetooth.active_connections")}`
: nothing}
</span>
${!isRemoteScanner
? html`<ha-icon-button
slot="meta"
.path=${mdiCogOutline}
.entry=${entry}
@click=${this._openOptionFlow}
.label=${this.hass.localize(
"ui.panel.config.bluetooth.option_flow"
)}
></ha-icon-button>`
: nothing}
</ha-list-item>
${hasMismatch
? this._renderScannerMismatchWarning(
entry.title,
scannerState,
scannerType
)
: nothing}
`;
});
}
private _renderScannerMismatchWarning(
name: string,
scannerState: BluetoothScannerState,
scannerType: HaScannerType,
formatMode: (mode: string | null) => string
scannerType: HaScannerType
) {
const instructions: string[] = [];
@@ -238,8 +262,9 @@ 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),
name: name,
requested: this._formatMode(scannerState.requested_mode),
current: this._formatMode(scannerState.current_mode),
}
)}
</div>
@@ -249,127 +274,59 @@ export class BluetoothConfigDashboard extends LitElement {
</ha-alert>`;
}
private _renderScannerState() {
if (!this._configEntry || !this._scannerState) {
return html`<div>
${this.hass.localize(
"ui.panel.config.bluetooth.no_scanner_state_available"
)}
</div>`;
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
}
};
return html`
<div class="scanner-state">
<div class="state-row">
<span
>${this.hass.localize(
"ui.panel.config.bluetooth.current_scanning_mode"
)}:</span
>
<span class="state-value"
>${formatMode(scannerState.current_mode)}</span
>
</div>
<div class="state-row">
<span
>${this.hass.localize(
"ui.panel.config.bluetooth.requested_scanning_mode"
)}:</span
>
<span class="state-value"
>${formatMode(scannerState.requested_mode)}</span
>
</div>
${scannerState.current_mode !== scannerState.requested_mode
? this._renderScannerMismatchWarning(
scannerState,
scannerType,
formatMode
)
: nothing}
</div>
`;
}
private _renderConnectionAllocations() {
if (this._connectionAllocationsError) {
return html`<ha-alert alert-type="error"
>${this._connectionAllocationsError}</ha-alert
>`;
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;
}
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=${allocations.allocated.length > 0
? `${allocationsUsed}/${allocationsTotal} (${allocations.allocated.join(", ")})`
: `${allocationsUsed}/${allocationsTotal}`}
></ha-metric>
`;
}
private async _openOptionFlow() {
const configEntryId = this._configEntry;
if (!configEntryId) {
return;
private _formatScannerModeText(
scannerState: BluetoothScannerState | undefined
): string {
if (!scannerState) {
return this.hass.localize(
"ui.panel.config.bluetooth.scanner_state_unknown"
);
}
const configEntries = await getConfigEntries(this.hass, {
domain: "bluetooth",
});
const configEntry = configEntries.find(
(entry) => entry.entry_id === configEntryId
);
showOptionsFlowDialog(this, configEntry!);
return this._formatModeLabel(scannerState.current_mode);
}
private _openOptionFlow(ev: Event) {
const button = ev.currentTarget as HTMLElement & { entry: ConfigEntry };
showOptionsFlowDialog(this, button.entry);
}
static get styles(): CSSResultGroup {
@@ -394,17 +351,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;
}
`,
];

View File

@@ -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}

View File

@@ -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}
>
<ha-network-graph
.hass=${this.hass}

View File

@@ -1,5 +1,4 @@
import { customElement, property } from "lit/decorators";
import { navigate } from "../../../../../common/navigate";
import type { RouterOptions } from "../../../../../layouts/hass-router-page";
import { HassRouterPage } from "../../../../../layouts/hass-router-page";
import type { HomeAssistant } from "../../../../../types";
@@ -12,10 +11,6 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
@property({ type: Boolean }) public narrow = false;
private _configEntry = new URLSearchParams(window.location.search).get(
"config_entry"
);
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
showLoading: true,
@@ -52,7 +47,6 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
el.hass = this.hass;
el.isWide = this.isWide;
el.narrow = this.narrow;
el.configEntryId = this._configEntry;
if (this._currentPage === "group") {
el.groupId = this.routeTail.path.substr(1);
} else if (this._currentPage === "device") {
@@ -60,17 +54,6 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
} else if (this._currentPage === "visualization") {
el.zoomedDeviceIdFromURL = this.routeTail.path.substr(1);
}
const searchParams = new URLSearchParams(window.location.search);
if (this._configEntry && !searchParams.has("config_entry")) {
searchParams.append("config_entry", this._configEntry);
navigate(
`${this.routeTail.prefix}${
this.routeTail.path
}?${searchParams.toString()}`,
{ replace: true }
);
}
}
}

View File

@@ -75,7 +75,7 @@ class ZHAConfigDashboard extends LitElement {
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ attribute: false }) public configEntryId?: string;
@state() private _configEntry?: ConfigEntry;
@state() private _configuration?: ZHAConfiguration;
@@ -95,6 +95,7 @@ class ZHAConfigDashboard extends LitElement {
super.firstUpdated(changedProperties);
if (this.hass) {
this.hass.loadBackendTranslation("config_panel", "zha", false);
this._fetchConfigEntry();
this._fetchConfiguration();
this._fetchSettings();
this._fetchDevicesAndUpdateStatus();
@@ -110,7 +111,6 @@ class ZHAConfigDashboard extends LitElement {
.narrow=${this.narrow}
.route=${this.route}
.tabs=${zhaTabs}
back-path="/config/integrations"
has-fab
>
<div class="container">
@@ -151,28 +151,26 @@ class ZHAConfigDashboard extends LitElement {
</div>
</div>
</div>
${this.configEntryId
? html`<div class="card-actions">
<ha-button
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this.configEntryId}`}
appearance="plain"
size="small"
>
${this.hass.localize(
"ui.panel.config.devices.caption"
)}</ha-button
>
<ha-button
appearance="plain"
size="small"
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this.configEntryId}`}
>
${this.hass.localize(
"ui.panel.config.entities.caption"
)}</ha-button
>
</div>`
: ""}
<div class="card-actions">
<ha-button
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
appearance="plain"
size="small"
>
${this.hass.localize(
"ui.panel.config.devices.caption"
)}</ha-button
>
<ha-button
appearance="plain"
size="small"
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
>
${this.hass.localize(
"ui.panel.config.entities.caption"
)}</ha-button
>
</div>
</ha-card>
<ha-card
class="network-settings"
@@ -321,6 +319,15 @@ class ZHAConfigDashboard extends LitElement {
`;
}
private async _fetchConfigEntry(): Promise<void> {
const configEntries = await getConfigEntries(this.hass, {
domain: "zha",
});
if (configEntries.length) {
this._configEntry = configEntries[0];
}
}
private async _fetchConfiguration(): Promise<void> {
this._configuration = await fetchZHAConfiguration(this.hass!);
}
@@ -399,20 +406,11 @@ class ZHAConfigDashboard extends LitElement {
fileDownload(backupJSON, `${basename}.json`);
}
private async _openOptionFlow() {
if (!this.configEntryId) {
private _openOptionFlow() {
if (!this._configEntry) {
return;
}
const configEntries: ConfigEntry[] = await getConfigEntries(this.hass, {
domain: "zha",
});
const configEntry = configEntries.find(
(entry) => entry.entry_id === this.configEntryId
);
showOptionsFlowDialog(this, configEntry!);
showOptionsFlowDialog(this, this._configEntry);
}
private _dataChanged(ev) {

View File

@@ -186,7 +186,7 @@ export class HuiEnergyDevicesGraphCard
params.value[0] as number,
this.hass.locale,
params.value < 0.1 ? { maximumFractionDigits: 3 } : undefined
)} kWh`;
)} kWh ${params.percent ? `(${params.percent} %)` : ""}`;
return `${title}${params.marker} ${params.seriesName}: <div style="direction:ltr; display: inline;">${value}</div>`;
}

View File

@@ -28,11 +28,16 @@ import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
import { DEFAULT_HOURS_TO_SHOW, DEFAULT_ZOOM } from "../../cards/hui-map-card";
import type { MapCardConfig, MapEntityConfig } from "../../cards/types";
import "../../components/hui-entity-editor";
import type { EntityConfig } from "../../entity-rows/types";
import "../hui-sub-element-editor";
import type {
EditDetailElementEvent,
SubElementEditorConfig,
EntitiesEditorEvent,
} from "../types";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import type { LovelaceCardEditor } from "../../types";
import { processEditorEntities } from "../process-editor-entities";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import type { EntitiesEditorEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
export const mapEntitiesConfigStruct = union([
@@ -76,13 +81,20 @@ const cardConfigStruct = assign(
const themeModes = ["auto", "light", "dark"] as const;
const SUB_SCHEMA = [
{ name: "entity", selector: { entity: {} }, required: true },
{ name: "name", selector: { text: {} } },
] as const;
@customElement("hui-map-card-editor")
export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: MapCardConfig;
@state() private _configEntities?: EntityConfig[];
@state() private _subElementEditorConfig?: SubElementEditorConfig;
@state() private _configEntities?: MapEntityConfig[];
@state() private _possibleGeoSources?: { value: string; label?: string }[];
@@ -150,7 +162,7 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
this._config = config;
this._configEntities = config.entities
? processEditorEntities(config.entities)
? (processEditorEntities(config.entities) as MapEntityConfig[])
: [];
}
@@ -167,6 +179,19 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
return nothing;
}
if (this._subElementEditorConfig) {
return html`
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
.schema=${SUB_SCHEMA}
@go-back=${this._goBack}
@config-changed=${this._handleSubEntityChanged}
>
</hui-sub-element-editor>
`;
}
return html`
<ha-form
.hass=${this.hass}
@@ -180,7 +205,9 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
.hass=${this.hass}
.entities=${this._configEntities}
.entityFilter=${hasLocation}
can-edit
@entities-changed=${this._entitiesValueChanged}
@edit-detail-element=${this._editDetailElement}
></hui-entity-editor>
<h3>
@@ -203,6 +230,36 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
`;
}
private _goBack(): void {
this._subElementEditorConfig = undefined;
}
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
this._subElementEditorConfig = ev.detail.subElementConfig;
}
private _handleSubEntityChanged(ev: CustomEvent): void {
ev.stopPropagation();
const index = this._subElementEditorConfig!.index!;
const newEntities = this._configEntities!.concat();
const newConfig = ev.detail.config as MapEntityConfig;
this._subElementEditorConfig = {
...this._subElementEditorConfig!,
elementConfig: newConfig,
};
newEntities[index] = newConfig;
let config = this._config!;
config = { ...config, entities: newEntities };
this._config = config;
this._configEntities = processEditorEntities(
config.entities as any[]
) as MapEntityConfig[];
fireEvent(this, "config-changed", { config });
}
private _selectSchema = memoizeOne(
(options, localize: LocalizeFunc): SelectSelector => ({
select: {
@@ -229,7 +286,9 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
if (ev.detail && ev.detail.entities) {
this._config = { ...this._config!, entities: ev.detail.entities };
this._configEntities = processEditorEntities(this._config.entities || []);
this._configEntities = processEditorEntities(
this._config.entities || []
) as MapEntityConfig[];
fireEvent(this, "config-changed", { config: this._config! });
}
}

View File

@@ -263,7 +263,8 @@ class HUIRoot extends LitElement {
{
icon: mdiPlus,
key: "ui.panel.lovelace.menu.add",
visible: !this._editMode && this.hass.user?.is_admin,
visible:
!this._editMode && this.hass.user?.is_admin && !this.hass.kioskMode,
overflow: this.narrow,
subItems: [
{
@@ -301,7 +302,7 @@ class HUIRoot extends LitElement {
key: "ui.panel.lovelace.menu.search_entities",
buttonAction: this._showQuickBar,
overflowAction: this._handleShowQuickBar,
visible: !this._editMode,
visible: !this._editMode && !this.hass.kioskMode,
overflow: this.narrow,
suffix:
this.hass.enableShortcuts && !isMobileClient ? "(E)" : undefined,
@@ -349,7 +350,8 @@ class HUIRoot extends LitElement {
visible:
!this._editMode &&
this.hass!.user?.is_admin &&
!this.hass!.config.recovery_mode,
!this.hass!.config.recovery_mode &&
!this.hass.kioskMode,
overflow: true,
overflow_can_promote: true,
},

View File

@@ -6012,27 +6012,26 @@
"retain": "Retain"
},
"bluetooth": {
"title": "Bluetooth",
"settings_title": "Bluetooth settings",
"tabs": {
"overview": "Overview",
"advertisements": "Advertisements",
"visualization": "Visualization",
"connections": "Connections"
},
"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.",
"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_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.",
"no_scanner_state_available": "No scanner state available",
"current_scanning_mode": "Current scanning mode",
"requested_scanning_mode": "Requested scanning mode",
"scanner_state_unknown": "State unknown",
"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",
@@ -6050,7 +6049,6 @@
"service_uuids": "Service UUIDs",
"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"

View File

@@ -1670,6 +1670,18 @@ __metadata:
languageName: node
linkType: hard
"@exodus/bytes@npm:^1.6.0":
version: 1.6.0
resolution: "@exodus/bytes@npm:1.6.0"
peerDependencies:
"@exodus/crypto": ^1.0.0-rc.4
peerDependenciesMeta:
"@exodus/crypto":
optional: true
checksum: 10/4066bc5f2b7782fabdad4cac707031cbe7c3491bcd38f28e3b5144d687f858e834aaa9b0bcbe9685f1ccfaf4dc747172e82f28cf361e9623448ec80ab755b198
languageName: node
linkType: hard
"@floating-ui/core@npm:^1.7.3":
version: 1.7.3
resolution: "@floating-ui/core@npm:1.7.3"
@@ -9126,7 +9138,7 @@ __metadata:
idb-keyval: "npm:6.2.2"
intl-messageformat: "npm:11.0.7"
js-yaml: "npm:4.1.1"
jsdom: "npm:27.3.0"
jsdom: "npm:27.4.0"
jszip: "npm:3.10.1"
leaflet: "npm:1.9.4"
leaflet-draw: "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
@@ -9209,12 +9221,12 @@ __metadata:
languageName: node
linkType: hard
"html-encoding-sniffer@npm:^4.0.0":
version: 4.0.0
resolution: "html-encoding-sniffer@npm:4.0.0"
"html-encoding-sniffer@npm:^6.0.0":
version: 6.0.0
resolution: "html-encoding-sniffer@npm:6.0.0"
dependencies:
whatwg-encoding: "npm:^3.1.1"
checksum: 10/e86efd493293a5671b8239bd099d42128433bb3c7b0fdc7819282ef8e118a21f5dead0ad6f358e024a4e5c84f17ebb7a9b36075220fac0a6222b207248bede6f
"@exodus/bytes": "npm:^1.6.0"
checksum: 10/97392e45d8aff57f180f62a1b12e62201c8451af68424b8bc3196f78e273891f2df285e5be43a3f28c7ba4badf9524ef305db65c4e4935a9e796afc86d9654b8
languageName: node
linkType: hard
@@ -9389,7 +9401,7 @@ __metadata:
languageName: node
linkType: hard
"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3":
"iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3":
version: 0.6.3
resolution: "iconv-lite@npm:0.6.3"
dependencies:
@@ -10185,16 +10197,17 @@ __metadata:
languageName: node
linkType: hard
"jsdom@npm:27.3.0":
version: 27.3.0
resolution: "jsdom@npm:27.3.0"
"jsdom@npm:27.4.0":
version: 27.4.0
resolution: "jsdom@npm:27.4.0"
dependencies:
"@acemir/cssom": "npm:^0.9.28"
"@asamuzakjp/dom-selector": "npm:^6.7.6"
"@exodus/bytes": "npm:^1.6.0"
cssstyle: "npm:^5.3.4"
data-urls: "npm:^6.0.0"
decimal.js: "npm:^10.6.0"
html-encoding-sniffer: "npm:^4.0.0"
html-encoding-sniffer: "npm:^6.0.0"
http-proxy-agent: "npm:^7.0.2"
https-proxy-agent: "npm:^7.0.6"
is-potential-custom-element-name: "npm:^1.0.1"
@@ -10204,7 +10217,6 @@ __metadata:
tough-cookie: "npm:^6.0.0"
w3c-xmlserializer: "npm:^5.0.0"
webidl-conversions: "npm:^8.0.0"
whatwg-encoding: "npm:^3.1.1"
whatwg-mimetype: "npm:^4.0.0"
whatwg-url: "npm:^15.1.0"
ws: "npm:^8.18.3"
@@ -10214,7 +10226,7 @@ __metadata:
peerDependenciesMeta:
canvas:
optional: true
checksum: 10/c650e954df04a80e7309984450ce764ae2a840810b9575b20204194dee3c5cff42e65526519cb58b946ffe66a058b7b763bad4814b3903a9c86a2c1651b8b74b
checksum: 10/7c6db85ab91183b95204648e086cfc09ecee36d9e8fee0bb5d68e27543eca632de0af6d43de461176a7823820543d5c53561778af5f712b1a1cd28bfac084d51
languageName: node
linkType: hard
@@ -14801,15 +14813,6 @@ __metadata:
languageName: node
linkType: hard
"whatwg-encoding@npm:^3.1.1":
version: 3.1.1
resolution: "whatwg-encoding@npm:3.1.1"
dependencies:
iconv-lite: "npm:0.6.3"
checksum: 10/bbef815eb67f91487c7f2ef96329743f5fd8357d7d62b1119237d25d41c7e452dff8197235b2d3c031365a17f61d3bb73ca49d0ed1582475aa4a670815e79534
languageName: node
linkType: hard
"whatwg-fetch@npm:^3.4.1":
version: 3.6.20
resolution: "whatwg-fetch@npm:3.6.20"