From b656ddc1f05666f89352acc2b1970ddacda022ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 30 Apr 2025 14:51:22 +0200 Subject: [PATCH 01/80] Add DHCP Browser entry point to network (#25235) * Add DHCP Browser entry point to network * lint --- .../config/network/ha-config-network-dhcp.ts | 66 +++++++++++++++++++ .../network/ha-config-section-network.ts | 7 ++ src/translations/en.json | 3 + 3 files changed, 76 insertions(+) create mode 100644 src/panels/config/network/ha-config-network-dhcp.ts diff --git a/src/panels/config/network/ha-config-network-dhcp.ts b/src/panels/config/network/ha-config-network-dhcp.ts new file mode 100644 index 0000000000..ef70d11903 --- /dev/null +++ b/src/panels/config/network/ha-config-network-dhcp.ts @@ -0,0 +1,66 @@ +import "@material/mwc-button/mwc-button"; +import type { CSSResultGroup } from "lit"; +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import "../../../components/ha-button"; +import "../../../components/ha-card"; +import { haStyle } from "../../../resources/styles"; +import type { HomeAssistant } from "../../../types"; + +@customElement("ha-config-network-dhcp") +class ConfigNetworkDHCP extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + protected render() { + return html` + +
+

+ ${this.hass.localize("ui.panel.config.network.discovery.dhcp_info")} +

+
+ +
+ `; + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + ha-settings-row { + padding: 0; + } + + .card-actions { + display: flex; + flex-direction: row-reverse; + justify-content: space-between; + align-items: center; + } + `, // row-reverse so we tab first to "save" + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-config-network-dhcp": ConfigNetworkDHCP; + } +} diff --git a/src/panels/config/network/ha-config-section-network.ts b/src/panels/config/network/ha-config-section-network.ts index 4b5f6a2808..c9a3df311d 100644 --- a/src/panels/config/network/ha-config-section-network.ts +++ b/src/panels/config/network/ha-config-section-network.ts @@ -5,6 +5,7 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import "../../../layouts/hass-subpage"; import type { HomeAssistant, Route } from "../../../types"; import "./ha-config-network"; +import "./ha-config-network-dhcp"; import "./ha-config-network-ssdp"; import "./ha-config-network-zeroconf"; import "./ha-config-url-form"; @@ -37,6 +38,11 @@ class HaConfigSectionNetwork extends LitElement { : ""} + ${isComponentLoaded(this.hass, "dhcp") + ? html`` + : ""} ${isComponentLoaded(this.hass, "ssdp") ? html` Date: Wed, 30 Apr 2025 15:57:16 +0300 Subject: [PATCH 02/80] Allow pasting more automation config formats (#25239) --- .../automation/manual-automation-editor.ts | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/src/panels/config/automation/manual-automation-editor.ts b/src/panels/config/automation/manual-automation-editor.ts index a064b65a8d..57583f0505 100644 --- a/src/panels/config/automation/manual-automation-editor.ts +++ b/src/panels/config/automation/manual-automation-editor.ts @@ -27,7 +27,7 @@ import type { Trigger, } from "../../../data/automation"; import { normalizeAutomationConfig } from "../../../data/automation"; -import type { Action } from "../../../data/script"; +import { getActionType, type Action } from "../../../data/script"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; @@ -312,10 +312,59 @@ export class HaManualAutomationEditor extends LitElement { const loaded: any = load(paste); if (loaded) { - let normalized: AutomationConfig | undefined; + let config = loaded; + + if ("automation" in config) { + config = config.automation; + if (Array.isArray(config)) { + config = config[0]; + } + } + + if (Array.isArray(config)) { + if (config.length === 1) { + config = config[0]; + } else { + const newConfig: AutomationConfig = { + triggers: [], + conditions: [], + actions: [], + }; + let found = false; + config.forEach((cfg: any) => { + if ("trigger" in cfg) { + found = true; + (newConfig.triggers as Trigger[]).push(cfg); + } + if ("condition" in cfg) { + found = true; + (newConfig.conditions as Condition[]).push(cfg); + } + if (getActionType(cfg) !== "unknown") { + found = true; + (newConfig.actions as Action[]).push(cfg); + } + }); + if (found) { + config = newConfig; + } + } + } + + if ("trigger" in config) { + config = { triggers: [config] }; + } + if ("condition" in config) { + config = { conditions: [config] }; + } + if (getActionType(config) !== "unknown") { + config = { actions: [config] }; + } + + let normalized: AutomationConfig; try { - normalized = normalizeAutomationConfig(loaded); + normalized = normalizeAutomationConfig(config); } catch (_err: any) { return; } From 50e39de9740fdf901e14a9c3ec0a84b5266fb943 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 30 Apr 2025 16:15:10 +0300 Subject: [PATCH 03/80] Allow pasting more script config (#25240) * Allow pasting more script config * Update manual-script-editor.ts --- .../config/script/manual-script-editor.ts | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/panels/config/script/manual-script-editor.ts b/src/panels/config/script/manual-script-editor.ts index d15c41a49f..457a6f7303 100644 --- a/src/panels/config/script/manual-script-editor.ts +++ b/src/panels/config/script/manual-script-editor.ts @@ -24,7 +24,11 @@ import "../../../components/ha-card"; import "../../../components/ha-icon-button"; import "../../../components/ha-markdown"; import type { Action, Fields, ScriptConfig } from "../../../data/script"; -import { MODES, normalizeScriptConfig } from "../../../data/script"; +import { + getActionType, + MODES, + normalizeScriptConfig, +} from "../../../data/script"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; @@ -236,10 +240,31 @@ export class HaManualScriptEditor extends LitElement { const loaded: any = load(paste); if (loaded) { + let config = loaded; + + if ("script" in config) { + config = config.script; + if (Object.keys(config).length) { + config = config[Object.keys(config)[0]]; + } + } + + if (Array.isArray(config)) { + if (config.length === 1) { + config = config[0]; + } else { + config = { sequence: config }; + } + } + + if (!["sequence", "unknown"].includes(getActionType(config))) { + config = { sequence: [config] }; + } + let normalized: ScriptConfig | undefined; try { - normalized = normalizeScriptConfig(loaded); + normalized = normalizeScriptConfig(config); } catch (_err: any) { return; } From 2e4ce71d0604164334fb46c83d41d5896f60ff7b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 30 Apr 2025 17:02:53 +0300 Subject: [PATCH 04/80] Improve trigger condition check on paste (#25241) --- src/data/automation.ts | 19 +++++++++++++++++++ .../automation/manual-automation-editor.ts | 14 +++++++++----- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/data/automation.ts b/src/data/automation.ts index 7308e51e34..1e57fde2ff 100644 --- a/src/data/automation.ts +++ b/src/data/automation.ts @@ -492,6 +492,25 @@ export const getAutomationEditorInitData = () => { return data; }; +export const isTrigger = (config: unknown): boolean => { + if (!config || typeof config !== "object") { + return false; + } + const trigger = config as Record; + return ( + ("trigger" in trigger && typeof trigger.trigger === "string") || + ("platform" in trigger && typeof trigger.platform === "string") + ); +}; + +export const isCondition = (config: unknown): boolean => { + if (!config || typeof config !== "object") { + return false; + } + const condition = config as Record; + return "condition" in condition && typeof condition.condition === "string"; +}; + export const subscribeTrigger = ( hass: HomeAssistant, onChange: (result: { diff --git a/src/panels/config/automation/manual-automation-editor.ts b/src/panels/config/automation/manual-automation-editor.ts index 57583f0505..8e0ae2209f 100644 --- a/src/panels/config/automation/manual-automation-editor.ts +++ b/src/panels/config/automation/manual-automation-editor.ts @@ -26,7 +26,11 @@ import type { ManualAutomationConfig, Trigger, } from "../../../data/automation"; -import { normalizeAutomationConfig } from "../../../data/automation"; +import { + isCondition, + isTrigger, + normalizeAutomationConfig, +} from "../../../data/automation"; import { getActionType, type Action } from "../../../data/script"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; @@ -332,11 +336,11 @@ export class HaManualAutomationEditor extends LitElement { }; let found = false; config.forEach((cfg: any) => { - if ("trigger" in cfg) { + if (isTrigger(cfg)) { found = true; (newConfig.triggers as Trigger[]).push(cfg); } - if ("condition" in cfg) { + if (isCondition(cfg)) { found = true; (newConfig.conditions as Condition[]).push(cfg); } @@ -351,10 +355,10 @@ export class HaManualAutomationEditor extends LitElement { } } - if ("trigger" in config) { + if (isTrigger(config)) { config = { triggers: [config] }; } - if ("condition" in config) { + if (isCondition(config)) { config = { conditions: [config] }; } if (getActionType(config) !== "unknown") { From e5f41ceb3e5a0d95880193ea59a25fcd41596544 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 30 Apr 2025 16:08:42 +0200 Subject: [PATCH 05/80] Bumped version to 20250430.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 169b86dbe8..bb4fba543b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20250430.0" +version = "20250430.1" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend" From 46cc254f773a65b35a545743df41ddf90abfe5bf Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 30 Apr 2025 16:17:01 +0200 Subject: [PATCH 06/80] Use code font family variable in combo-box (#25243) --- src/components/entity/ha-entity-combo-box.ts | 2 +- src/components/entity/ha-statistic-combo-box.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/entity/ha-entity-combo-box.ts b/src/components/entity/ha-entity-combo-box.ts index 2aa96604f6..9a89fe23d3 100644 --- a/src/components/entity/ha-entity-combo-box.ts +++ b/src/components/entity/ha-entity-combo-box.ts @@ -71,7 +71,7 @@ const DOMAIN_STYLE = styleMap({ }); const ENTITY_ID_STYLE = styleMap({ - fontFamily: "var(--code-font-family, monospace)", + fontFamily: "var(--ha-font-family-code)", fontSize: "var(--ha-font-size-xs)", }); diff --git a/src/components/entity/ha-statistic-combo-box.ts b/src/components/entity/ha-statistic-combo-box.ts index c6ec0621d0..313bececcb 100644 --- a/src/components/entity/ha-statistic-combo-box.ts +++ b/src/components/entity/ha-statistic-combo-box.ts @@ -48,7 +48,7 @@ interface StatisticItem { const TYPE_ORDER = ["entity", "external", "no_state"] as StatisticItemType[]; const ENTITY_ID_STYLE = styleMap({ - fontFamily: "var(--code-font-family, monospace)", + fontFamily: "var(--ha-font-family-code)", fontSize: "11px", }); From efd7b380a94bcdcec11cc2982e772268f3a23115 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 30 Apr 2025 18:45:35 +0300 Subject: [PATCH 07/80] Handle errrors/wrong values on paste (#25245) --- src/components/ha-toast.ts | 4 + .../automation/manual-automation-editor.ts | 195 ++++++++++-------- .../config/script/manual-script-editor.ts | 125 ++++++----- src/translations/en.json | 2 + 4 files changed, 181 insertions(+), 145 deletions(-) diff --git a/src/components/ha-toast.ts b/src/components/ha-toast.ts index f0f94cbea4..8946aee00c 100644 --- a/src/components/ha-toast.ts +++ b/src/components/ha-toast.ts @@ -24,6 +24,10 @@ export class HaToast extends Snackbar { max-width: 650px; } + .mdc-snackbar__actions { + color: rgba(255, 255, 255, 0.87); + } + /* Revert the default styles set by mwc-snackbar */ @media (max-width: 480px), (max-width: 344px) { .mdc-snackbar__surface { diff --git a/src/panels/config/automation/manual-automation-editor.ts b/src/panels/config/automation/manual-automation-editor.ts index 8e0ae2209f..ad113fbef0 100644 --- a/src/panels/config/automation/manual-automation-editor.ts +++ b/src/panels/config/automation/manual-automation-editor.ts @@ -314,104 +314,119 @@ export class HaManualAutomationEditor extends LitElement { return; } - const loaded: any = load(paste); - if (loaded) { - let config = loaded; + let loaded: any; + try { + loaded = load(paste); + } catch (_err: any) { + showToast(this, { + message: this.hass.localize( + "ui.panel.config.automation.editor.paste_invalid_yaml" + ), + duration: 4000, + dismissable: true, + }); + return; + } - if ("automation" in config) { - config = config.automation; - if (Array.isArray(config)) { - config = config[0]; - } - } + if (!loaded || typeof loaded !== "object") { + return; + } + let config = loaded; + + if ("automation" in config) { + config = config.automation; if (Array.isArray(config)) { - if (config.length === 1) { - config = config[0]; - } else { - const newConfig: AutomationConfig = { - triggers: [], - conditions: [], - actions: [], - }; - let found = false; - config.forEach((cfg: any) => { - if (isTrigger(cfg)) { - found = true; - (newConfig.triggers as Trigger[]).push(cfg); - } - if (isCondition(cfg)) { - found = true; - (newConfig.conditions as Condition[]).push(cfg); - } - if (getActionType(cfg) !== "unknown") { - found = true; - (newConfig.actions as Action[]).push(cfg); - } - }); - if (found) { - config = newConfig; + config = config[0]; + } + } + + if (Array.isArray(config)) { + if (config.length === 1) { + config = config[0]; + } else { + const newConfig: AutomationConfig = { + triggers: [], + conditions: [], + actions: [], + }; + let found = false; + config.forEach((cfg: any) => { + if (isTrigger(cfg)) { + found = true; + (newConfig.triggers as Trigger[]).push(cfg); + } + if (isCondition(cfg)) { + found = true; + (newConfig.conditions as Condition[]).push(cfg); + } + if (getActionType(cfg) !== "unknown") { + found = true; + (newConfig.actions as Action[]).push(cfg); } - } - } - - if (isTrigger(config)) { - config = { triggers: [config] }; - } - if (isCondition(config)) { - config = { conditions: [config] }; - } - if (getActionType(config) !== "unknown") { - config = { actions: [config] }; - } - - let normalized: AutomationConfig; - - try { - normalized = normalizeAutomationConfig(config); - } catch (_err: any) { - return; - } - - try { - assert(normalized, automationConfigStruct); - } catch (_err: any) { - showToast(this, { - message: this.hass.localize( - "ui.panel.config.automation.editor.paste_invalid_config" - ), - duration: 4000, - dismissable: true, }); - return; - } - - if (normalized) { - ev.preventDefault(); - - if (this.dirty) { - const result = await new Promise((resolve) => { - showPasteReplaceDialog(this, { - domain: "automation", - pastedConfig: normalized, - onClose: () => resolve(false), - onAppend: () => { - this._appendToExistingConfig(normalized); - resolve(false); - }, - onReplace: () => resolve(true), - }); - }); - - if (!result) { - return; - } + if (found) { + config = newConfig; } - - // replace the config completely - this._replaceExistingConfig(normalized); } } + + if (isTrigger(config)) { + config = { triggers: [config] }; + } + if (isCondition(config)) { + config = { conditions: [config] }; + } + if (getActionType(config) !== "unknown") { + config = { actions: [config] }; + } + + let normalized: AutomationConfig; + + try { + normalized = normalizeAutomationConfig(config); + } catch (_err: any) { + return; + } + + try { + assert(normalized, automationConfigStruct); + } catch (_err: any) { + showToast(this, { + message: this.hass.localize( + "ui.panel.config.automation.editor.paste_invalid_config" + ), + duration: 4000, + dismissable: true, + }); + return; + } + + if (normalized) { + ev.preventDefault(); + + if (this.dirty) { + const result = await new Promise((resolve) => { + showPasteReplaceDialog(this, { + domain: "automation", + pastedConfig: normalized, + onClose: () => resolve(false), + onAppend: () => { + this._appendToExistingConfig(normalized); + resolve(false); + }, + onReplace: () => resolve(true), + }); + }); + + if (!result) { + return; + } + } + + // replace the config completely + this._replaceExistingConfig(normalized); + } }; private _appendToExistingConfig(config: ManualAutomationConfig) { diff --git a/src/panels/config/script/manual-script-editor.ts b/src/panels/config/script/manual-script-editor.ts index 457a6f7303..9799855cda 100644 --- a/src/panels/config/script/manual-script-editor.ts +++ b/src/panels/config/script/manual-script-editor.ts @@ -238,75 +238,90 @@ export class HaManualScriptEditor extends LitElement { return; } - const loaded: any = load(paste); - if (loaded) { - let config = loaded; + let loaded: any; + try { + loaded = load(paste); + } catch (_err: any) { + showToast(this, { + message: this.hass.localize( + "ui.panel.config.script.editor.paste_invalid_config" + ), + duration: 4000, + dismissable: true, + }); + return; + } - if ("script" in config) { - config = config.script; - if (Object.keys(config).length) { - config = config[Object.keys(config)[0]]; - } + if (!loaded || typeof loaded !== "object") { + return; + } + + let config = loaded; + + if ("script" in config) { + config = config.script; + if (Object.keys(config).length) { + config = config[Object.keys(config)[0]]; } + } - if (Array.isArray(config)) { - if (config.length === 1) { - config = config[0]; - } else { - config = { sequence: config }; - } + if (Array.isArray(config)) { + if (config.length === 1) { + config = config[0]; + } else { + config = { sequence: config }; } + } - if (!["sequence", "unknown"].includes(getActionType(config))) { - config = { sequence: [config] }; - } + if (!["sequence", "unknown"].includes(getActionType(config))) { + config = { sequence: [config] }; + } - let normalized: ScriptConfig | undefined; + let normalized: ScriptConfig | undefined; - try { - normalized = normalizeScriptConfig(config); - } catch (_err: any) { - return; - } + try { + normalized = normalizeScriptConfig(config); + } catch (_err: any) { + return; + } - try { - assert(normalized, scriptConfigStruct); - } catch (_err: any) { - showToast(this, { - message: this.hass.localize( - "ui.panel.config.script.editor.paste_invalid_config" - ), - duration: 4000, - dismissable: true, - }); - return; - } + try { + assert(normalized, scriptConfigStruct); + } catch (_err: any) { + showToast(this, { + message: this.hass.localize( + "ui.panel.config.script.editor.paste_invalid_config" + ), + duration: 4000, + dismissable: true, + }); + return; + } - if (normalized) { - ev.preventDefault(); + if (normalized) { + ev.preventDefault(); - if (this.dirty) { - const result = await new Promise((resolve) => { - showPasteReplaceDialog(this, { - domain: "script", - pastedConfig: normalized, - onClose: () => resolve(false), - onAppend: () => { - this._appendToExistingConfig(normalized); - resolve(false); - }, - onReplace: () => resolve(true), - }); + if (this.dirty) { + const result = await new Promise((resolve) => { + showPasteReplaceDialog(this, { + domain: "script", + pastedConfig: normalized, + onClose: () => resolve(false), + onAppend: () => { + this._appendToExistingConfig(normalized); + resolve(false); + }, + onReplace: () => resolve(true), }); + }); - if (!result) { - return; - } + if (!result) { + return; } - - // replace the config completely - this._replaceExistingConfig(normalized); } + + // replace the config completely + this._replaceExistingConfig(normalized); } }; diff --git a/src/translations/en.json b/src/translations/en.json index d9d5daaf90..1fb0d53531 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4356,6 +4356,7 @@ "text": "How do you want to paste your automation?" }, "paste_toast_message": "Pasted automation from clipboard", + "paste_invalid_yaml": "Pasted value is not valid YAML", "paste_invalid_config": "Pasted automation is not editable in the visual editor" }, "trace": { @@ -4595,6 +4596,7 @@ "text": "How do you want to paste your script?" }, "paste_toast_message": "Pasted script from clipboard", + "paste_invalid_yaml": "Pasted value is not valid YAML", "paste_invalid_config": "Pasted script is not editable in the visual editor" }, "trace": { From ee9cbf73704ed3f034864a805a660b9b3d6e8873 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 30 Apr 2025 17:50:44 +0200 Subject: [PATCH 08/80] Bumped version to 20250430.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bb4fba543b..cb3fd4db2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20250430.1" +version = "20250430.2" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend" From 1a14511fa6bf8774ef6dc9b48c5a9001cfd32fcc Mon Sep 17 00:00:00 2001 From: Yosi Levy <37745463+yosilevy@users.noreply.github.com> Date: Wed, 30 Apr 2025 22:07:55 +0300 Subject: [PATCH 09/80] Various RTL fixes (#25231) --- src/components/media-player/ha-media-player-browse.ts | 6 ++++++ src/dialogs/config-flow/step-flow-form.ts | 2 ++ .../voice-assistant-setup/voice-assistant-setup-dialog.ts | 1 + src/panels/config/devices/ha-config-device-page.ts | 1 + .../lovelace/dashboards/ha-config-lovelace-dashboards.ts | 2 +- 5 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/media-player/ha-media-player-browse.ts b/src/components/media-player/ha-media-player-browse.ts index d183d49b0d..78d5f1be51 100644 --- a/src/components/media-player/ha-media-player-browse.ts +++ b/src/components/media-player/ha-media-player-browse.ts @@ -890,12 +890,18 @@ export class HaMediaPlayerBrowse extends LitElement { display: flex; flex-direction: row-reverse; margin-right: 48px; + margin-inline-end: 48px; + margin-inline-start: initial; + direction: var(--direction); } .highlight-add-button ha-svg-icon { position: relative; top: -0.5em; margin-left: 8px; + margin-inline-start: 8px; + margin-inline-end: initial; + transform: scaleX(var(--scale-direction)); } .content { diff --git a/src/dialogs/config-flow/step-flow-form.ts b/src/dialogs/config-flow/step-flow-form.ts index a36f4a7152..90b301b98a 100644 --- a/src/dialogs/config-flow/step-flow-form.ts +++ b/src/dialogs/config-flow/step-flow-form.ts @@ -278,7 +278,9 @@ class StepFlowForm extends LitElement { } h2 { word-break: break-word; + padding-right: 72px; padding-inline-end: 72px; + padding-inline-start: initial; direction: var(--direction); } `, diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-dialog.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-dialog.ts index 7b292fe9ed..699d9099dd 100644 --- a/src/dialogs/voice-assistant-setup/voice-assistant-setup-dialog.ts +++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-dialog.ts @@ -407,6 +407,7 @@ export class HaVoiceAssistantSetupDialog extends LitElement { align-items: center; margin-right: 12px; margin-inline-end: 12px; + margin-inline-start: initial; } `, ]; diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 726d314042..af4033b42f 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -1559,6 +1559,7 @@ export class HaConfigDevicePage extends LitElement { align-items: center; padding-left: 8px; padding-inline-start: 8px; + padding-inline-end: initial; direction: var(--direction); } diff --git a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts index d81f74925c..56f1a398f6 100644 --- a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts +++ b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts @@ -161,7 +161,7 @@ export class HaConfigLovelaceDashboards extends LitElement { placement="right" > From 69eaf178cae43c673ef2c1b39152f4c998ae7ab1 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:47:51 -0700 Subject: [PATCH 10/80] New energy calculation formula (#25242) * New energy calculation * more tests and stricter tests. change priority order --- src/data/energy.ts | 100 +++++++++------- test/data/energy.test.ts | 239 ++++++++++++++++++++++++++++----------- 2 files changed, 234 insertions(+), 105 deletions(-) diff --git a/src/data/energy.ts b/src/data/energy.ts index 70aece7feb..88d318e9ed 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -959,21 +959,13 @@ const computeConsumptionDataPartial = ( }; data.timestamps.forEach((t) => { - const used_total = - (data.from_grid?.[t] || 0) + - (data.solar?.[t] || 0) + - (data.from_battery?.[t] || 0) - - (data.to_grid?.[t] || 0) - - (data.to_battery?.[t] || 0); - - outData.used_total[t] = used_total; - outData.total.used_total += used_total; const { grid_to_battery, battery_to_grid, used_solar, used_grid, used_battery, + used_total, solar_to_battery, solar_to_grid, } = computeConsumptionSingle({ @@ -984,6 +976,8 @@ const computeConsumptionDataPartial = ( from_battery: data.from_battery && (data.from_battery[t] ?? 0), }); + outData.used_total[t] = used_total; + outData.total.used_total += used_total; outData.grid_to_battery[t] = grid_to_battery; outData.total.grid_to_battery += grid_to_battery; outData.battery_to_grid![t] = battery_to_grid; @@ -1017,12 +1011,20 @@ export const computeConsumptionSingle = (data: { used_solar: number; used_grid: number; used_battery: number; + used_total: number; } => { - const to_grid = data.to_grid; - const to_battery = data.to_battery; - const solar = data.solar; - const from_grid = data.from_grid; - const from_battery = data.from_battery; + let to_grid = Math.max(data.to_grid || 0, 0); + let to_battery = Math.max(data.to_battery || 0, 0); + let solar = Math.max(data.solar || 0, 0); + let from_grid = Math.max(data.from_grid || 0, 0); + let from_battery = Math.max(data.from_battery || 0, 0); + + const used_total = + (from_grid || 0) + + (solar || 0) + + (from_battery || 0) - + (to_grid || 0) - + (to_battery || 0); let used_solar = 0; let grid_to_battery = 0; @@ -1031,41 +1033,57 @@ export const computeConsumptionSingle = (data: { let solar_to_grid = 0; let used_battery = 0; let used_grid = 0; - if ((to_grid != null || to_battery != null) && solar != null) { - used_solar = (solar || 0) - (to_grid || 0) - (to_battery || 0); - if (used_solar < 0) { - if (to_battery != null) { - grid_to_battery = used_solar * -1; - if (grid_to_battery > (from_grid || 0)) { - battery_to_grid = grid_to_battery - (from_grid || 0); - grid_to_battery = from_grid || 0; - } - } - used_solar = 0; - } - } - if (from_battery != null) { - used_battery = (from_battery || 0) - battery_to_grid; - } + let used_total_remaining = Math.max(used_total, 0); + // Consumption Priority + // Battery_Out -> Grid_Out + // Solar -> Grid_Out + // Solar -> Battery_In + // Grid_In -> Battery_In + // Solar -> Consumption + // Battery_Out -> Consumption + // Grid_In -> Consumption - if (from_grid != null) { - used_grid = from_grid - grid_to_battery; - } + // Battery_Out -> Grid_Out + battery_to_grid = Math.min(from_battery, to_grid); + from_battery -= battery_to_grid; + to_grid -= battery_to_grid; - if (solar != null) { - if (to_battery != null) { - solar_to_battery = Math.max(0, (to_battery || 0) - grid_to_battery); - } - if (to_grid != null) { - solar_to_grid = Math.max(0, (to_grid || 0) - battery_to_grid); - } - } + // Solar -> Grid_Out + solar_to_grid = Math.min(solar, to_grid); + to_grid -= solar_to_grid; + solar -= solar_to_grid; + + // Solar -> Battery_In + solar_to_battery = Math.min(solar, to_battery); + to_battery -= solar_to_battery; + solar -= solar_to_battery; + + // Grid_In -> Battery_In + grid_to_battery = Math.min(from_grid, to_battery); + from_grid -= grid_to_battery; + to_battery -= to_battery; + + // Solar -> Consumption + used_solar = Math.min(used_total_remaining, solar); + used_total_remaining -= used_solar; + solar -= used_solar; + + // Battery_Out -> Consumption + used_battery = Math.min(from_battery, used_total_remaining); + from_battery -= used_battery; + used_total_remaining -= used_battery; + + // Grid_In -> Consumption + used_grid = Math.min(used_total_remaining, from_grid); + from_grid -= used_grid; + used_total_remaining -= from_grid; return { used_solar, used_grid, used_battery, + used_total, grid_to_battery, battery_to_grid, solar_to_battery, diff --git a/test/data/energy.test.ts b/test/data/energy.test.ts index e5fcbe9dc6..5609803ced 100644 --- a/test/data/energy.test.ts +++ b/test/data/energy.test.ts @@ -14,6 +14,47 @@ import { } from "../../src/data/energy"; import type { HomeAssistant } from "../../src/types"; +const checkConsumptionResult = ( + input: { + from_grid: number | undefined; + to_grid: number | undefined; + solar: number | undefined; + to_battery: number | undefined; + from_battery: number | undefined; + }, + exact = true +): { + grid_to_battery: number; + battery_to_grid: number; + solar_to_battery: number; + solar_to_grid: number; + used_solar: number; + used_grid: number; + used_battery: number; + used_total: number; +} => { + const result = computeConsumptionSingle(input); + if (exact) { + assert.equal( + result.used_total, + result.used_solar + result.used_battery + result.used_grid + ); + assert.equal( + input.to_grid || 0, + result.solar_to_grid + result.battery_to_grid + ); + assert.equal( + input.to_battery || 0, + result.grid_to_battery + result.solar_to_battery + ); + assert.equal( + input.solar || 0, + result.solar_to_battery + result.solar_to_grid + result.used_solar + ); + } + return result; +}; + describe("Energy Short Format Test", () => { // Create default to not have to specify a not relevant TimeFormat over and over again. const defaultLocale: FrontendLocaleData = { @@ -88,7 +129,7 @@ describe("Energy Usage Calculation Tests", () => { it("Consuming Energy From the Grid", () => { [0, 5, 1000].forEach((x) => { assert.deepEqual( - computeConsumptionSingle({ + checkConsumptionResult({ from_grid: x, to_grid: undefined, solar: undefined, @@ -101,6 +142,7 @@ describe("Energy Usage Calculation Tests", () => { used_solar: 0, used_grid: x, used_battery: 0, + used_total: x, solar_to_battery: 0, solar_to_grid: 0, } @@ -108,61 +150,78 @@ describe("Energy Usage Calculation Tests", () => { }); }); it("Solar production, consuming some and returning the remainder to grid.", () => { - [2.99, 3, 10, 100].forEach((s) => { + ( + [ + [2.99, false], // unsolveable : solar < to_grid + [3, true], + [10, true], + [100, true], + ] as any + ).forEach(([s, exact]) => { assert.deepEqual( - computeConsumptionSingle({ - from_grid: 0, - to_grid: 3, - solar: s, - to_battery: undefined, - from_battery: undefined, - }), + checkConsumptionResult( + { + from_grid: 0, + to_grid: 3, + solar: s, + to_battery: undefined, + from_battery: undefined, + }, + exact + ), { grid_to_battery: 0, battery_to_grid: 0, - used_solar: Math.max(0, s - 3), + used_solar: Math.min(s, Math.max(0, s - 3)), used_grid: 0, used_battery: 0, + used_total: s - 3, solar_to_battery: 0, - solar_to_grid: 3, + solar_to_grid: Math.min(3, s), } ); }); }); it("Solar production with simultaneous grid consumption. Excess solar returned to the grid.", () => { - [ - [0, 0], - [3, 0], - [0, 3], - [5, 4], - [4, 5], - [10, 3], - [3, 7], - [3, 7.1], - ].forEach(([from_grid, to_grid]) => { + ( + [ + [0, 0, true], + [3, 0, true], + [0, 3, true], + [5, 4, true], + [4, 5, true], + [10, 3, true], + [3, 7, true], + [3, 7.1, false], // unsolveable: to_grid > solar + ] as any + ).forEach(([from_grid, to_grid, exact]) => { assert.deepEqual( - computeConsumptionSingle({ - from_grid, - to_grid, - solar: 7, - to_battery: undefined, - from_battery: undefined, - }), + checkConsumptionResult( + { + from_grid, + to_grid, + solar: 7, + to_battery: undefined, + from_battery: undefined, + }, + exact + ), { grid_to_battery: 0, battery_to_grid: 0, used_solar: Math.max(0, 7 - to_grid), - used_grid: from_grid, + used_grid: from_grid - Math.max(0, to_grid - 7), + used_total: from_grid - to_grid + 7, used_battery: 0, solar_to_battery: 0, - solar_to_grid: to_grid, + solar_to_grid: Math.min(7, to_grid), } ); }); }); it("Charging the battery from the grid", () => { assert.deepEqual( - computeConsumptionSingle({ + checkConsumptionResult({ from_grid: 5, to_grid: 0, solar: 0, @@ -175,6 +234,7 @@ describe("Energy Usage Calculation Tests", () => { used_solar: 0, used_grid: 2, used_battery: 0, + used_total: 2, solar_to_battery: 0, solar_to_grid: 0, } @@ -182,7 +242,7 @@ describe("Energy Usage Calculation Tests", () => { }); it("Consuming from the grid and battery simultaneously", () => { assert.deepEqual( - computeConsumptionSingle({ + checkConsumptionResult({ from_grid: 5, to_grid: 0, solar: 0, @@ -195,6 +255,7 @@ describe("Energy Usage Calculation Tests", () => { used_solar: 0, used_grid: 5, used_battery: 5, + used_total: 10, solar_to_battery: 0, solar_to_grid: 0, } @@ -202,7 +263,7 @@ describe("Energy Usage Calculation Tests", () => { }); it("Consuming some battery and returning some battery to the grid", () => { assert.deepEqual( - computeConsumptionSingle({ + checkConsumptionResult({ from_grid: 0, to_grid: 4, solar: 0, @@ -215,15 +276,15 @@ describe("Energy Usage Calculation Tests", () => { used_solar: 0, used_grid: 0, used_battery: 1, + used_total: 1, solar_to_battery: 0, solar_to_grid: 0, } ); }); - /* Fails it("Charging and discharging the battery to/from the grid in the same interval.", () => { assert.deepEqual( - computeConsumptionSingle({ + checkConsumptionResult({ from_grid: 5, to_grid: 1, solar: 0, @@ -234,15 +295,18 @@ describe("Energy Usage Calculation Tests", () => { grid_to_battery: 3, battery_to_grid: 1, used_solar: 0, - used_grid: 1, + used_grid: 2, used_battery: 0, + used_total: 2, + solar_to_battery: 0, + solar_to_grid: 0, } ); - }); */ - /* Test does not pass, battery is not really correct when solar is not present + }); + it("Charging the battery with no solar sensor.", () => { assert.deepEqual( - computeConsumptionSingle({ + checkConsumptionResult({ from_grid: 5, to_grid: 0, solar: undefined, @@ -255,13 +319,15 @@ describe("Energy Usage Calculation Tests", () => { used_solar: 0, used_grid: 2, used_battery: 0, + used_total: 2, + solar_to_battery: 0, + solar_to_grid: 0, } ); - }); */ - /* Test does not pass + }); it("Discharging battery to grid while also consuming from grid.", () => { assert.deepEqual( - computeConsumptionSingle({ + checkConsumptionResult({ from_grid: 5, to_grid: 4, solar: 0, @@ -274,14 +340,16 @@ describe("Energy Usage Calculation Tests", () => { used_solar: 0, used_grid: 5, used_battery: 0, + used_total: 5, + solar_to_grid: 0, + solar_to_battery: 0, } ); }); - */ it("Grid, solar, and battery", () => { assert.deepEqual( - computeConsumptionSingle({ + checkConsumptionResult({ from_grid: 5, to_grid: 3, solar: 7, @@ -294,12 +362,13 @@ describe("Energy Usage Calculation Tests", () => { used_solar: 1, used_grid: 5, used_battery: 0, + used_total: 6, solar_to_battery: 3, solar_to_grid: 3, } ); assert.deepEqual( - computeConsumptionSingle({ + checkConsumptionResult({ from_grid: 5, to_grid: 3, solar: 7, @@ -308,16 +377,17 @@ describe("Energy Usage Calculation Tests", () => { }), { grid_to_battery: 0, - battery_to_grid: 0, - used_solar: 1, + battery_to_grid: 3, + used_solar: 4, used_grid: 5, - used_battery: 10, + used_battery: 7, + used_total: 16, solar_to_battery: 3, - solar_to_grid: 3, + solar_to_grid: 0, } ); assert.deepEqual( - computeConsumptionSingle({ + checkConsumptionResult({ from_grid: 2, to_grid: 7, solar: 7, @@ -325,17 +395,18 @@ describe("Energy Usage Calculation Tests", () => { from_battery: 1, }), { - grid_to_battery: 1, - battery_to_grid: 0, + grid_to_battery: 0, + battery_to_grid: 1, used_solar: 0, - used_grid: 1, - used_battery: 1, - solar_to_battery: 0, - solar_to_grid: 7, + used_grid: 2, + used_battery: 0, + used_total: 2, + solar_to_battery: 1, + solar_to_grid: 6, } ); assert.deepEqual( - computeConsumptionSingle({ + checkConsumptionResult({ from_grid: 2, to_grid: 7, solar: 9, @@ -344,17 +415,17 @@ describe("Energy Usage Calculation Tests", () => { }), { grid_to_battery: 0, - battery_to_grid: 0, - used_solar: 1, + battery_to_grid: 1, + used_solar: 2, used_grid: 2, - used_battery: 1, + used_battery: 0, + used_total: 4, solar_to_battery: 1, - solar_to_grid: 7, + solar_to_grid: 6, } ); - /* Test does not pass assert.deepEqual( - computeConsumptionSingle({ + checkConsumptionResult({ from_grid: 5, to_grid: 3, solar: 1, @@ -367,8 +438,48 @@ describe("Energy Usage Calculation Tests", () => { used_solar: 0, used_grid: 5, used_battery: 0, + used_total: 5, + solar_to_battery: 0, + solar_to_grid: 1, + } + ); + assert.deepEqual( + checkConsumptionResult({ + from_grid: 6, + to_grid: 0, + solar: 3, + to_battery: 6, + from_battery: 6, + }), + { + grid_to_battery: 3, + battery_to_grid: 0, + used_solar: 0, + used_grid: 3, + used_battery: 6, + solar_to_battery: 3, + solar_to_grid: 0, + used_total: 9, + } + ); + assert.deepEqual( + checkConsumptionResult({ + from_grid: 0, + to_grid: 1, + solar: 1, + to_battery: 1, + from_battery: 1, + }), + { + grid_to_battery: 0, + battery_to_grid: 1, + used_solar: 0, + used_grid: 0, + used_battery: 0, + solar_to_battery: 1, + solar_to_grid: 0, + used_total: 0, } ); - */ }); }); From 9b7e2886b6633efa7c714233af30bc83e0374fe1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 30 Apr 2025 21:10:32 +0200 Subject: [PATCH 11/80] Better explain when DHCP discovery data will be available (#25250) It was pointed out that users likely may not know what DHCP is and wonder why the data is not available yet. --- src/translations/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/translations/en.json b/src/translations/en.json index 1fb0d53531..82c4d93ba9 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -6395,7 +6395,7 @@ }, "discovery": { "dhcp": "DHCP browser", - "dhcp_info": "The DHCP browser displays devices discovered by Home Assistant via DHCP, ARP+PTR lookups, and router-based device trackers. All detected devices by these methods will appear here.", + "dhcp_info": "The DHCP browser shows devices detected by Home Assistant using methods like DHCP, ARP+PTR lookups, and router-based device trackers. DHCP (Dynamic Host Configuration Protocol) data is received when devices join the network and request an IP address, allowing Home Assistant to discover them automatically. All devices found through these methods will appear here.", "dhcp_browser": "View DHCP browser", "ssdp": "SSDP browser", "ssdp_info": "The SSDP browser shows devices discovered by Home Assistant using SSDP/UPnP. Devices that Home Assistant has discovered will appear here.", From b81d2013dc27afeec0660d067ed8ffece3497f07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 30 Apr 2025 21:16:25 +0200 Subject: [PATCH 12/80] Fix formatting of mac address fields in device info (#25251) Previous change lost the `:` seperator, and unexpectedly MAC became title case instead of MAC when dhcp is loaded. --- .../config/devices/device-detail/ha-device-info-card.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panels/config/devices/device-detail/ha-device-info-card.ts b/src/panels/config/devices/device-detail/ha-device-info-card.ts index dba2bbfa66..29a4b678b8 100644 --- a/src/panels/config/devices/device-detail/ha-device-info-card.ts +++ b/src/panels/config/devices/device-detail/ha-device-info-card.ts @@ -106,7 +106,7 @@ export class HaDeviceCard extends LitElement {
${type === "bluetooth" && isComponentLoaded(this.hass, "bluetooth") - ? html`${titleCase(type)} + ? html`${titleCase(type)}: Date: Wed, 30 Apr 2025 21:53:07 +0200 Subject: [PATCH 13/80] Improve message when no discovery data is found (#25252) * Improve message when no discovery data is found It was pointed out its a bit confusing when a device has not been discovered yet for the discovery/network browser panels as we only said there was no data. Give the user a better hint as to why there is no data. * Improve message when no discovery data is found It was pointed out its a bit confusing when a device has not been discovered yet for the discovery/network browser panels as we only said there was no data. Give the user a better hint as to why there is no data. * Improve message when no discovery data is found It was pointed out its a bit confusing when a device has not been discovered yet for the discovery/network browser panels as we only said there was no data. Give the user a better hint as to why there is no data. * Improve message when no discovery data is found It was pointed out its a bit confusing when a device has not been discovered yet for the discovery/network browser panels as we only said there was no data. Give the user a better hint as to why there is no data. --- .../bluetooth/bluetooth-advertisement-monitor.ts | 3 +++ .../integration-panels/dhcp/dhcp-config-panel.ts | 3 +++ .../integration-panels/ssdp/ssdp-config-panel.ts | 3 +++ .../zeroconf/zeroconf-config-panel.ts | 3 +++ src/translations/en.json | 10 +++++++--- 5 files changed, 19 insertions(+), 3 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 5bcd7389f4..d9e53f3b05 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 @@ -210,6 +210,9 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement { .route=${this.route} .columns=${this._columns(this.hass.localize)} .data=${this._dataWithNamedSourceAndIds(this._data)} + .noDataText=${this.hass.localize( + "ui.panel.config.bluetooth.no_advertisements_found" + )} @row-click=${this._handleRowClicked} .initialGroupColumn=${this._activeGrouping} .initialCollapsedGroups=${this._activeCollapsed} diff --git a/src/panels/config/integrations/integration-panels/dhcp/dhcp-config-panel.ts b/src/panels/config/integrations/integration-panels/dhcp/dhcp-config-panel.ts index ea4c24a2aa..683719ee43 100644 --- a/src/panels/config/integrations/integration-panels/dhcp/dhcp-config-panel.ts +++ b/src/panels/config/integrations/integration-panels/dhcp/dhcp-config-panel.ts @@ -96,6 +96,9 @@ export class DHCPConfigPanel extends SubscribeMixin(LitElement) { .route=${this.route} .columns=${this._columns(this.hass.localize)} .data=${this._dataWithIds(this._data)} + .noDataText=${this.hass.localize( + "ui.panel.config.dhcp.no_devices_found" + )} filter=${this._macAddress || ""} > `; diff --git a/src/panels/config/integrations/integration-panels/ssdp/ssdp-config-panel.ts b/src/panels/config/integrations/integration-panels/ssdp/ssdp-config-panel.ts index 215205cfe8..1433e2d582 100644 --- a/src/panels/config/integrations/integration-panels/ssdp/ssdp-config-panel.ts +++ b/src/panels/config/integrations/integration-panels/ssdp/ssdp-config-panel.ts @@ -105,6 +105,9 @@ export class SSDPConfigPanel extends SubscribeMixin(LitElement) { @grouping-changed=${this._handleGroupingChanged} @collapsed-changed=${this._handleCollapseChanged} .data=${this._dataWithIds(this._data)} + .noDataText=${this.hass.localize( + "ui.panel.config.ssdp.no_devices_found" + )} @row-click=${this._handleRowClicked} clickable > diff --git a/src/panels/config/integrations/integration-panels/zeroconf/zeroconf-config-panel.ts b/src/panels/config/integrations/integration-panels/zeroconf/zeroconf-config-panel.ts index 57ffabcdee..64a237fde1 100644 --- a/src/panels/config/integrations/integration-panels/zeroconf/zeroconf-config-panel.ts +++ b/src/panels/config/integrations/integration-panels/zeroconf/zeroconf-config-panel.ts @@ -112,6 +112,9 @@ export class ZeroconfConfigPanel extends SubscribeMixin(LitElement) { @grouping-changed=${this._handleGroupingChanged} @collapsed-changed=${this._handleCollapseChanged} .data=${this._dataWithIds(this._data)} + .noDataText=${this.hass.localize( + "ui.panel.config.zeroconf.no_devices_found" + )} @row-click=${this._handleRowClicked} clickable > diff --git a/src/translations/en.json b/src/translations/en.json index 82c4d93ba9..c47cd875ad 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5508,6 +5508,7 @@ "connection_monitor": "Connection monitor", "used_connection_slot_allocations": "Used connection slot allocations", "no_connections": "No active 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.", "address": "Address", @@ -5528,7 +5529,8 @@ "title": "DHCP discovery", "mac_address": "MAC Address", "hostname": "Hostname", - "ip_address": "IP Address" + "ip_address": "IP Address", + "no_devices_found": "No recent DHCP requests found; no matching discoveries detected" }, "thread": { "other_networks": "Other networks", @@ -5577,7 +5579,8 @@ "ssdp_headers": "SSDP Headers", "upnp": "Universal Plug and Play (UPnP)", "discovery_information": "Discovery information", - "copy_to_clipboard": "Copy to clipboard" + "copy_to_clipboard": "Copy to clipboard", + "no_devices_found": "No matching SSDP/UPnP discoveries found" }, "zeroconf": { "name": "Name", @@ -5586,7 +5589,8 @@ "ip_addresses": "IP Addresses", "properties": "Properties", "discovery_information": "Discovery information", - "copy_to_clipboard": "Copy to clipboard" + "copy_to_clipboard": "Copy to clipboard", + "no_devices_found": "No matching Zeroconf discoveries found" }, "zha": { "common": { From 9e5b7462aff35e2c7be9316db2641928cc3962a7 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 30 Apr 2025 22:23:45 +0200 Subject: [PATCH 14/80] Add `?` as shortcut for shortcuts dialog (#25253) Bind shortcuts dialog to `?` key --- src/state/quick-bar-mixin.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/state/quick-bar-mixin.ts b/src/state/quick-bar-mixin.ts index 598d064597..6e931a9bd7 100644 --- a/src/state/quick-bar-mixin.ts +++ b/src/state/quick-bar-mixin.ts @@ -15,6 +15,7 @@ import type { HassElement } from "./hass-element"; import { extractSearchParamsObject } from "../common/url/search-params"; import { showVoiceCommandDialog } from "../dialogs/voice-command-dialog/show-ha-voice-command-dialog"; import { canOverrideAlphanumericInput } from "../common/dom/can-override-input"; +import { showShortcutsDialog } from "../dialogs/shortcuts/show-shortcuts-dialog"; declare global { interface HASSDomEvents { @@ -51,6 +52,8 @@ export default >(superClass: T) => case "a": this._showVoiceCommandDialog(ev.detail); break; + case "?": + this._showShortcutDialog(ev.detail); } }); @@ -65,6 +68,8 @@ export default >(superClass: T) => m: (ev) => this._createMyLink(ev), a: (ev) => this._showVoiceCommandDialog(ev), d: (ev) => this._showQuickBar(ev, QuickBarMode.Device), + // Workaround see https://github.com/jamiebuilds/tinykeys/issues/130 + "Shift+?": (ev) => this._showShortcutDialog(ev), // Those are fallbacks for non-latin keyboards that don't have e, c, m keys (qwerty-based shortcuts) KeyE: (ev) => this._showQuickBar(ev), KeyC: (ev) => this._showQuickBar(ev, QuickBarMode.Command), @@ -111,6 +116,19 @@ export default >(superClass: T) => showQuickBar(this, { mode }); } + private _showShortcutDialog(e: KeyboardEvent) { + if (!this._canShowQuickBar(e)) { + return; + } + + if (e.defaultPrevented) { + return; + } + e.preventDefault(); + + showShortcutsDialog(this); + } + private async _createMyLink(e: KeyboardEvent) { if ( !this.hass?.enableShortcuts || From 2b06742bb9c2a72d7815f78444f25175b23b6318 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 1 May 2025 09:25:18 +0200 Subject: [PATCH 15/80] Hide the tab when view is a subview (#25256) --- src/panels/lovelace/hui-root.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index 883fab5089..8083dd947c 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -300,6 +300,12 @@ class HUIRoot extends LitElement { const background = curViewConfig?.background || this.config.background; + const _isTabHiddenForUser = (view: LovelaceViewConfig) => + view.visible !== undefined && + ((Array.isArray(view.visible) && + !view.visible.some((e) => e.user === this.hass!.user?.id)) || + view.visible === false); + const tabs = html` ${views.map( (view, index) => html` @@ -311,13 +317,7 @@ class HUIRoot extends LitElement { class=${classMap({ icon: Boolean(view.icon), "hide-tab": Boolean( - !this._editMode && - view.visible !== undefined && - ((Array.isArray(view.visible) && - !view.visible.some( - (e) => e.user === this.hass!.user?.id - )) || - view.visible === false) + !this._editMode && (view.subview || _isTabHiddenForUser(view)) ), })} > From f24b6a4cb1556d852857a3935fa07389bc870fe6 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Wed, 30 Apr 2025 21:27:11 -0700 Subject: [PATCH 16/80] Fix typo in energy calculation (#25259) * New energy calculation * more tests and stricter tests. change priority order * more test and fix error --- src/data/energy.ts | 2 +- test/data/energy.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/data/energy.ts b/src/data/energy.ts index 88d318e9ed..06498ace9b 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -1062,7 +1062,7 @@ export const computeConsumptionSingle = (data: { // Grid_In -> Battery_In grid_to_battery = Math.min(from_grid, to_battery); from_grid -= grid_to_battery; - to_battery -= to_battery; + to_battery -= grid_to_battery; // Solar -> Consumption used_solar = Math.min(used_total_remaining, solar); diff --git a/test/data/energy.test.ts b/test/data/energy.test.ts index 5609803ced..ba8d159e08 100644 --- a/test/data/energy.test.ts +++ b/test/data/energy.test.ts @@ -462,6 +462,8 @@ describe("Energy Usage Calculation Tests", () => { used_total: 9, } ); + }); + it("Solar -> Battery -> Grid", () => { assert.deepEqual( checkConsumptionResult({ from_grid: 0, @@ -482,4 +484,25 @@ describe("Energy Usage Calculation Tests", () => { } ); }); + it("Solar -> Grid && Grid -> Battery", () => { + assert.deepEqual( + checkConsumptionResult({ + from_grid: 1, + to_grid: 1, + solar: 1, + to_battery: 1, + from_battery: 0, + }), + { + grid_to_battery: 1, + battery_to_grid: 0, + used_solar: 0, + used_grid: 0, + used_battery: 0, + solar_to_battery: 0, + solar_to_grid: 1, + used_total: 0, + } + ); + }); }); From 4624cc609f0dd0d128c7b50ae7809e0c95f7547d Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 1 May 2025 19:02:55 +0200 Subject: [PATCH 17/80] Always show backup location retention settings (#25261) Always show backup location retention settings --- .../backup/ha-config-backup-location.ts | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/panels/config/backup/ha-config-backup-location.ts b/src/panels/config/backup/ha-config-backup-location.ts index 6e5d5b585f..323ca5097c 100644 --- a/src/panels/config/backup/ha-config-backup-location.ts +++ b/src/panels/config/backup/ha-config-backup-location.ts @@ -118,19 +118,17 @@ class HaConfigBackupDetails extends LitElement {

` - : this.config?.agents[this.agentId] - ? html`` - : nothing} + : html``}
From d7dd11ba7f941235f1fe6b7c2881d6c23d62b646 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 1 May 2025 20:06:42 +0300 Subject: [PATCH 18/80] Improve error handling in automation i18n (#25266) --- .../entity/ha-entity-attribute-picker.ts | 16 +- src/data/automation_i18n.ts | 167 ++++++++++-------- 2 files changed, 103 insertions(+), 80 deletions(-) diff --git a/src/components/entity/ha-entity-attribute-picker.ts b/src/components/entity/ha-entity-attribute-picker.ts index 2468f6fe71..7e08060e33 100644 --- a/src/components/entity/ha-entity-attribute-picker.ts +++ b/src/components/entity/ha-entity-attribute-picker.ts @@ -73,16 +73,20 @@ class HaEntityAttributePicker extends LitElement { return nothing; } + const stateObj = this.hass.states[this.entityId!] as HassEntity | undefined; + return html` @@ -988,12 +1003,14 @@ const tryDescribeCondition = ( ); const attribute = condition.attribute - ? computeAttributeNameDisplay( - hass.localize, - stateObj, - hass.entities, - condition.attribute - ) + ? stateObj + ? computeAttributeNameDisplay( + hass.localize, + stateObj, + hass.entities, + condition.attribute + ) + : condition.attribute : undefined; if (condition.above !== undefined && condition.below !== undefined) { @@ -1187,7 +1204,9 @@ const tryDescribeCondition = ( if (localized) { return localized; } - const stateObj = hass.states[config.entity_id as string]; + const stateObj = hass.states[config.entity_id as string] as + | HassEntity + | undefined; return `${stateObj ? computeStateName(stateObj) : config.entity_id} ${ config.type }`; From 5d89563aa581d243a96d96f3d1b5d17cb922e068 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 May 2025 08:57:56 -0400 Subject: [PATCH 19/80] Import missing components on init page (#25269) --- src/layouts/ha-init-page.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/layouts/ha-init-page.ts b/src/layouts/ha-init-page.ts index 84cd396569..af33535a32 100644 --- a/src/layouts/ha-init-page.ts +++ b/src/layouts/ha-init-page.ts @@ -1,6 +1,8 @@ import type { PropertyValues } from "lit"; import { css, html, LitElement } from "lit"; import { property, state } from "lit/decorators"; +import "@material/mwc-button"; +import "../components/ha-spinner"; class HaInitPage extends LitElement { @property({ type: Boolean }) public error = false; From fae619085c6e51ea640496f601662d2fb57084d3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 May 2025 05:46:36 -0500 Subject: [PATCH 20/80] Add my links for the Bluetooth monitors (#25270) The plan is to link these in the Bluetooth docs for help debugging --- src/panels/my/ha-panel-my.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/panels/my/ha-panel-my.ts b/src/panels/my/ha-panel-my.ts index 110e540fbb..cd78c52e55 100644 --- a/src/panels/my/ha-panel-my.ts +++ b/src/panels/my/ha-panel-my.ts @@ -110,6 +110,14 @@ export const getMyRedirects = (): Redirects => ({ component: "bluetooth", redirect: "/config/bluetooth", }, + bluetooth_advertisement_monitor: { + component: "bluetooth", + redirect: "/config/bluetooth/advertisement-monitor", + }, + bluetooth_connection_monitor: { + component: "bluetooth", + redirect: "/config/bluetooth/connection-monitor", + }, config_dhcp: { component: "dhcp", redirect: "/config/dhcp", From 32acef8fad77a65c9684dd7b8571a8eb62b3fa1c Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 2 May 2025 12:16:04 +0200 Subject: [PATCH 21/80] Add save shortcut to shortcuts dialog (#25271) --- src/dialogs/shortcuts/dialog-shortcuts.ts | 9 +++++++-- src/translations/en.json | 7 ++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/dialogs/shortcuts/dialog-shortcuts.ts b/src/dialogs/shortcuts/dialog-shortcuts.ts index 99a3710ba6..dbd85839ae 100644 --- a/src/dialogs/shortcuts/dialog-shortcuts.ts +++ b/src/dialogs/shortcuts/dialog-shortcuts.ts @@ -69,12 +69,17 @@ const _SHORTCUTS: Section[] = [ ], }, { - key: "ui.dialogs.shortcuts.automations.title", + key: "ui.dialogs.shortcuts.automation_script.title", items: [ { type: "shortcut", shortcut: [{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" }, "V"], - key: "ui.dialogs.shortcuts.automations.paste", + key: "ui.dialogs.shortcuts.automation_script.paste", + }, + { + type: "shortcut", + shortcut: [{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" }, "S"], + key: "ui.dialogs.shortcuts.automation_script.save", }, ], }, diff --git a/src/translations/en.json b/src/translations/en.json index c47cd875ad..af02f16e77 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1938,9 +1938,10 @@ "title": "Assist", "open_assist": "open Assist dialog" }, - "automations": { - "title": "Automations", - "paste": "to paste automation YAML from clipboard to automation editor" + "automation_script": { + "title": "Automations / Scripts", + "paste": "to paste automation/script YAML from clipboard to editor", + "save": "to save automation/script" }, "charts": { "title": "Charts", From 358e450e60981efe7110dcb73184f5cc444aff30 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Fri, 2 May 2025 06:19:12 -0700 Subject: [PATCH 22/80] Fix disabled language picker (#25278) --- src/components/ha-language-picker.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/ha-language-picker.ts b/src/components/ha-language-picker.ts index f771871756..9940b75184 100644 --- a/src/components/ha-language-picker.ts +++ b/src/components/ha-language-picker.ts @@ -102,7 +102,7 @@ export class HaLanguagePicker extends LitElement { localeChanged ) { this._select.layoutOptions(); - if (this._select.value !== this.value) { + if (!this.disabled && this._select.value !== this.value) { fireEvent(this, "value-changed", { value: this._select.value }); } if (!this.value) { @@ -141,7 +141,10 @@ export class HaLanguagePicker extends LitElement { ); const value = - this.value ?? (this.required ? languageOptions[0]?.value : this.value); + this.value ?? + (this.required && !this.disabled + ? languageOptions[0]?.value + : this.value); return html` Date: Fri, 2 May 2025 17:47:20 +0300 Subject: [PATCH 23/80] Fix alignment of ha-labeled-slider (#25279) --- src/components/ha-labeled-slider.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/ha-labeled-slider.ts b/src/components/ha-labeled-slider.ts index 8a54a14496..483d5aefd6 100644 --- a/src/components/ha-labeled-slider.ts +++ b/src/components/ha-labeled-slider.ts @@ -30,8 +30,9 @@ class HaLabeledSlider extends LitElement { @property({ type: Number }) public value?: number; protected render() { + const title = this._getTitle(); return html` -
${this._getTitle()}
+ ${title ? html`
${title}
` : nothing}
${this.icon ? html`` : nothing} @@ -73,17 +74,20 @@ class HaLabeledSlider extends LitElement { .slider-container { display: flex; + align-items: center; } ha-icon { - margin-top: 8px; color: var(--secondary-text-color); } ha-slider { + display: flex; flex-grow: 1; + align-items: center; background-image: var(--ha-slider-background); border-radius: 4px; + height: 32px; } `; } From a820cd45762532c8cd3962c7d6cea8ff3ea124b2 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 2 May 2025 19:21:38 +0300 Subject: [PATCH 24/80] Fix decorators with properties (#25282) * Fix decorators with properties * Green build --------- Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com> --- src/common/decorators/storage.ts | 12 ++---------- src/common/decorators/transform.ts | 14 +------------- src/components/ha-sidebar.ts | 2 ++ src/components/media-player/ha-browse-media-tts.ts | 1 + .../ha-voice-command-dialog.ts | 1 + src/panels/calendar/ha-panel-calendar.ts | 1 + .../ha-config-application-credentials.ts | 1 + .../automation/action/ha-automation-action.ts | 1 + .../condition/ha-automation-condition.ts | 1 + .../config/automation/ha-automation-editor.ts | 1 + .../config/automation/ha-automation-picker.ts | 2 ++ .../automation/option/ha-automation-option.ts | 1 + .../automation/trigger/ha-automation-trigger.ts | 1 + .../config/backup/ha-config-backup-backups.ts | 1 + .../config/blueprint/ha-blueprint-overview.ts | 9 ++++++--- .../config/devices/ha-config-devices-dashboard.ts | 2 ++ src/panels/config/entities/ha-config-entities.ts | 2 ++ src/panels/config/helpers/ha-config-helpers.ts | 1 + .../integration-panels/mqtt/mqtt-config-panel.ts | 7 ++++++- .../integration-panels/mqtt/mqtt-subscribe-card.ts | 3 +++ src/panels/config/labels/ha-config-labels.ts | 1 + .../dashboards/ha-config-lovelace-dashboards.ts | 1 + .../resources/ha-config-lovelace-resources.ts | 1 + src/panels/config/scene/ha-scene-dashboard.ts | 2 ++ src/panels/config/script/ha-script-editor.ts | 1 + src/panels/config/script/ha-script-picker.ts | 2 ++ src/panels/config/tags/ha-config-tags.ts | 1 + .../ha-config-voice-assistants-expose.ts | 1 + .../action/developer-tools-action.ts | 2 ++ .../assist/developer-tools-assist.ts | 1 + .../developer-tools/state/developer-tools-state.ts | 1 + src/panels/history/ha-panel-history.ts | 1 + src/panels/logbook/ha-panel-logbook.ts | 1 + .../energy/hui-energy-devices-detail-graph-card.ts | 1 + src/panels/lovelace/cards/hui-button-card.ts | 2 ++ .../editor/badge-editor/hui-badge-picker.ts | 1 + .../lovelace/editor/card-editor/hui-card-picker.ts | 1 + src/panels/media-browser/ha-panel-media-browser.ts | 2 ++ src/panels/todo/ha-panel-todo.ts | 3 ++- 39 files changed, 62 insertions(+), 28 deletions(-) diff --git a/src/common/decorators/storage.ts b/src/common/decorators/storage.ts index 82e67cd32f..03d4176548 100644 --- a/src/common/decorators/storage.ts +++ b/src/common/decorators/storage.ts @@ -1,6 +1,5 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { ReactiveElement } from "lit"; -import type { InternalPropertyDeclaration } from "lit/decorators"; +import type { ReactiveElement } from "lit"; type Callback = (oldValue: any, newValue: any) => void; @@ -108,7 +107,6 @@ export function storage(options: { storage?: "localStorage" | "sessionStorage"; subscribe?: boolean; state?: boolean; - stateOptions?: InternalPropertyDeclaration; serializer?: (value: any) => any; deserializer?: (value: any) => any; }) { @@ -174,7 +172,7 @@ export function storage(options: { performUpdate.call(this); }; - if (options.state && options.subscribe) { + if (options.subscribe) { const connectedCallback = proto.connectedCallback; const disconnectedCallback = proto.disconnectedCallback; @@ -192,12 +190,6 @@ export function storage(options: { el.__unbsubLocalStorage = undefined; }; } - if (options.state) { - ReactiveElement.createProperty(propertyKey, { - noAccessor: true, - ...options.stateOptions, - }); - } const descriptor = Object.getOwnPropertyDescriptor(proto, propertyKey); let newDescriptor: PropertyDescriptor; diff --git a/src/common/decorators/transform.ts b/src/common/decorators/transform.ts index ee02be719b..b6ac2717d4 100644 --- a/src/common/decorators/transform.ts +++ b/src/common/decorators/transform.ts @@ -1,10 +1,4 @@ -import { - ReactiveElement, - type PropertyDeclaration, - type PropertyValues, -} from "lit"; -import { shallowEqual } from "../util/shallow-equal"; - +import type { ReactiveElement, PropertyValues } from "lit"; /** * Transform function type. */ @@ -23,7 +17,6 @@ type ReactiveTransformElement = ReactiveElement & { export function transform(config: { transformer: Transformer; watch?: PropertyKey[]; - propertyOptions?: PropertyDeclaration; }) { return ( proto: ElemClass, @@ -84,11 +77,6 @@ export function transform(config: { curWatch.add(propertyKey); }); } - ReactiveElement.createProperty(propertyKey, { - noAccessor: true, - hasChanged: (v: any, o: any) => !shallowEqual(v, o), - ...config.propertyOptions, - }); const descriptor = Object.getOwnPropertyDescriptor(proto, propertyKey); let newDescriptor: PropertyDescriptor; diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 9b9695a765..b59b15405e 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -210,6 +210,7 @@ class HaSidebar extends SubscribeMixin(LitElement) { private _unsubPersistentNotifications: UnsubscribeFunc | undefined; + @state() @storage({ key: "sidebarPanelOrder", state: true, @@ -217,6 +218,7 @@ class HaSidebar extends SubscribeMixin(LitElement) { }) private _panelOrder: string[] = []; + @state() @storage({ key: "sidebarHiddenPanels", state: true, diff --git a/src/components/media-player/ha-browse-media-tts.ts b/src/components/media-player/ha-browse-media-tts.ts index 2e0be9526e..e6fbdd62d7 100644 --- a/src/components/media-player/ha-browse-media-tts.ts +++ b/src/components/media-player/ha-browse-media-tts.ts @@ -42,6 +42,7 @@ class BrowseMediaTTS extends LitElement { @state() private _provider?: TTSEngine; + @state() @storage({ key: "TtsMessage", state: true, diff --git a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts index 60aa04fb11..1b2a0e88a0 100644 --- a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts +++ b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts @@ -36,6 +36,7 @@ export class HaVoiceCommandDialog extends LitElement { @state() private _opened = false; + @state() @storage({ key: "AssistPipelineId", state: true, diff --git a/src/panels/calendar/ha-panel-calendar.ts b/src/panels/calendar/ha-panel-calendar.ts index b27b52874a..73cd194e4e 100644 --- a/src/panels/calendar/ha-panel-calendar.ts +++ b/src/panels/calendar/ha-panel-calendar.ts @@ -42,6 +42,7 @@ class PanelCalendar extends LitElement { @state() private _error?: string = undefined; + @state() @storage({ key: "deSelectedCalendars", state: true, diff --git a/src/panels/config/application_credentials/ha-config-application-credentials.ts b/src/panels/config/application_credentials/ha-config-application-credentials.ts index cb2fa6c5db..0ddb6f89da 100644 --- a/src/panels/config/application_credentials/ha-config-application-credentials.ts +++ b/src/panels/config/application_credentials/ha-config-application-credentials.ts @@ -69,6 +69,7 @@ export class HaConfigApplicationCredentials extends LitElement { }) private _activeHiddenColumns?: string[]; + @state() @storage({ storage: "sessionStorage", key: "application-credentials-table-search", diff --git a/src/panels/config/automation/action/ha-automation-action.ts b/src/panels/config/automation/action/ha-automation-action.ts index a70e45bac4..e41f9e500f 100644 --- a/src/panels/config/automation/action/ha-automation-action.ts +++ b/src/panels/config/automation/action/ha-automation-action.ts @@ -36,6 +36,7 @@ export default class HaAutomationAction extends LitElement { @state() private _showReorder = false; + @state() @storage({ key: "automationClipboard", state: true, diff --git a/src/panels/config/automation/condition/ha-automation-condition.ts b/src/panels/config/automation/condition/ha-automation-condition.ts index 5c3ed0f910..f1557a7956 100644 --- a/src/panels/config/automation/condition/ha-automation-condition.ts +++ b/src/panels/config/automation/condition/ha-automation-condition.ts @@ -36,6 +36,7 @@ export default class HaAutomationCondition extends LitElement { @state() private _showReorder = false; + @state() @storage({ key: "automationClipboard", state: true, diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index ebf232fc29..b6e2da8732 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -135,6 +135,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin( @state() private _blueprintConfig?: BlueprintAutomationConfig; + @state() @consume({ context: fullEntitiesContext, subscribe: true }) @transform({ transformer: function (this: HaAutomationEditor, value) { diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index 034fc12695..8f77c41275 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -138,6 +138,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { @state() private _filteredAutomations?: string[] | null; + @state() @storage({ storage: "sessionStorage", key: "automation-table-search", @@ -146,6 +147,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { }) private _filter = ""; + @state() @storage({ storage: "sessionStorage", key: "automation-table-filters-full", diff --git a/src/panels/config/automation/option/ha-automation-option.ts b/src/panels/config/automation/option/ha-automation-option.ts index 0bbe6e919d..8a3184a724 100644 --- a/src/panels/config/automation/option/ha-automation-option.ts +++ b/src/panels/config/automation/option/ha-automation-option.ts @@ -29,6 +29,7 @@ export default class HaAutomationOption extends LitElement { @state() private _showReorder = false; + @state() @storage({ key: "automationClipboard", state: true, diff --git a/src/panels/config/automation/trigger/ha-automation-trigger.ts b/src/panels/config/automation/trigger/ha-automation-trigger.ts index fabc23eef6..cc6219d0cc 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger.ts @@ -38,6 +38,7 @@ export default class HaAutomationTrigger extends LitElement { @state() private _showReorder = false; + @state() @storage({ key: "automationClipboard", state: true, diff --git a/src/panels/config/backup/ha-config-backup-backups.ts b/src/panels/config/backup/ha-config-backup-backups.ts index 30af43c705..969c99b275 100644 --- a/src/panels/config/backup/ha-config-backup-backups.ts +++ b/src/panels/config/backup/ha-config-backup-backups.ts @@ -98,6 +98,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { @state() private _selected: string[] = []; + @state() @storage({ storage: "sessionStorage", key: "backups-table-filters", diff --git a/src/panels/config/blueprint/ha-blueprint-overview.ts b/src/panels/config/blueprint/ha-blueprint-overview.ts index 6b329ff8e6..6ac304a956 100644 --- a/src/panels/config/blueprint/ha-blueprint-overview.ts +++ b/src/panels/config/blueprint/ha-blueprint-overview.ts @@ -9,7 +9,7 @@ import { } from "@mdi/js"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { LitElement, html } from "lit"; -import { customElement, property } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import type { HASSDomEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event"; @@ -118,6 +118,7 @@ class HaBlueprintOverview extends LitElement { }) private _activeHiddenColumns?: string[]; + @state() @storage({ storage: "sessionStorage", key: "blueprint-table-search", @@ -499,9 +500,11 @@ class HaBlueprintOverview extends LitElement { list: html`
    ${[...(related.automation || []), ...(related.script || [])].map( (item) => { - const state = this.hass.states[item]; + const automationState = this.hass.states[item]; return html`
  • - ${state ? `${computeStateName(state)} (${item})` : item} + ${automationState + ? `${computeStateName(automationState)} (${item})` + : item}
  • `; } )} diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index cd1d4fc6b1..5d6d467cec 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -120,6 +120,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { @state() private _selected: string[] = []; + @state() @storage({ storage: "sessionStorage", key: "devices-table-search", @@ -128,6 +129,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { }) private _filter: string = history.state?.filter || ""; + @state() @storage({ storage: "sessionStorage", key: "devices-table-filters-full", diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 4144c74571..807d03c380 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -159,6 +159,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { @consume({ context: fullEntitiesContext, subscribe: true }) _entities!: EntityRegistryEntry[]; + @state() @storage({ storage: "sessionStorage", key: "entities-table-search", @@ -169,6 +170,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { @state() private _searchParms = new URLSearchParams(window.location.search); + @state() @storage({ storage: "sessionStorage", key: "entities-table-filters", diff --git a/src/panels/config/helpers/ha-config-helpers.ts b/src/panels/config/helpers/ha-config-helpers.ts index 852720b1d8..b6de6abe4a 100644 --- a/src/panels/config/helpers/ha-config-helpers.ts +++ b/src/panels/config/helpers/ha-config-helpers.ts @@ -168,6 +168,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { }) private _activeCollapsed?: string; + @state() @storage({ storage: "sessionStorage", key: "helpers-table-search", diff --git a/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts b/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts index c87fd42e0d..c3f9302fe4 100644 --- a/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts +++ b/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.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 { storage } from "../../../../../common/decorators/storage"; import "../../../../../components/ha-card"; import "../../../../../components/ha-code-editor"; @@ -23,6 +23,7 @@ export class MQTTConfigPanel extends LitElement { @property({ type: Boolean }) public narrow = false; + @state() @storage({ key: "panel-dev-mqtt-topic-ls", state: true, @@ -30,6 +31,7 @@ export class MQTTConfigPanel extends LitElement { }) private _topic = ""; + @state() @storage({ key: "panel-dev-mqtt-payload-ls", state: true, @@ -37,6 +39,7 @@ export class MQTTConfigPanel extends LitElement { }) private _payload = ""; + @state() @storage({ key: "panel-dev-mqtt-qos-ls", state: true, @@ -44,6 +47,7 @@ export class MQTTConfigPanel extends LitElement { }) private _qos = "0"; + @state() @storage({ key: "panel-dev-mqtt-retain-ls", state: true, @@ -51,6 +55,7 @@ export class MQTTConfigPanel extends LitElement { }) private _retain = false; + @state() @storage({ key: "panel-dev-mqtt-allow-template-ls", state: true, diff --git a/src/panels/config/integrations/integration-panels/mqtt/mqtt-subscribe-card.ts b/src/panels/config/integrations/integration-panels/mqtt/mqtt-subscribe-card.ts index b9c917dea7..a0506ca54c 100644 --- a/src/panels/config/integrations/integration-panels/mqtt/mqtt-subscribe-card.ts +++ b/src/panels/config/integrations/integration-panels/mqtt/mqtt-subscribe-card.ts @@ -21,6 +21,7 @@ const qosLevel = ["0", "1", "2"]; class MqttSubscribeCard extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; + @state() @storage({ key: "panel-dev-mqtt-topic-subscribe", state: true, @@ -28,6 +29,7 @@ class MqttSubscribeCard extends LitElement { }) private _topic = ""; + @state() @storage({ key: "panel-dev-mqtt-qos-subscribe", state: true, @@ -35,6 +37,7 @@ class MqttSubscribeCard extends LitElement { }) private _qos = "0"; + @state() @storage({ key: "panel-dev-mqtt-json-format", state: true, diff --git a/src/panels/config/labels/ha-config-labels.ts b/src/panels/config/labels/ha-config-labels.ts index 77ed20ff72..0f6bfdc527 100644 --- a/src/panels/config/labels/ha-config-labels.ts +++ b/src/panels/config/labels/ha-config-labels.ts @@ -55,6 +55,7 @@ export class HaConfigLabels extends LitElement { @state() private _labels: LabelRegistryEntry[] = []; + @state() @storage({ storage: "sessionStorage", key: "labels-table-search", diff --git a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts index 56f1a398f6..1dce7fa90f 100644 --- a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts +++ b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts @@ -74,6 +74,7 @@ export class HaConfigLovelaceDashboards extends LitElement { @state() private _dashboards: LovelaceDashboard[] = []; + @state() @storage({ storage: "sessionStorage", key: "lovelace-dashboards-table-search", diff --git a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts index 73a7847f91..24feda1234 100644 --- a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts +++ b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts @@ -46,6 +46,7 @@ export class HaConfigLovelaceRescources extends LitElement { @state() private _resources: LovelaceResource[] = []; + @state() @storage({ storage: "sessionStorage", key: "lovelace-resources-table-search", diff --git a/src/panels/config/scene/ha-scene-dashboard.ts b/src/panels/config/scene/ha-scene-dashboard.ts index 01b2112f64..f01a23ce3b 100644 --- a/src/panels/config/scene/ha-scene-dashboard.ts +++ b/src/panels/config/scene/ha-scene-dashboard.ts @@ -133,6 +133,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { @state() private _filteredScenes?: string[] | null; + @state() @storage({ storage: "sessionStorage", key: "scene-table-search", @@ -141,6 +142,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { }) private _filter = ""; + @state() @storage({ storage: "sessionStorage", key: "scene-table-filters-full", diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index b4121a0125..a0e1b0c7ea 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -105,6 +105,7 @@ export class HaScriptEditor extends SubscribeMixin( @state() private _readOnly = false; + @state() @consume({ context: fullEntitiesContext, subscribe: true }) @transform({ transformer: function (this: HaScriptEditor, value) { diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index ef32f99ad7..3265cb158c 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -138,6 +138,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { @state() private _filteredScripts?: string[] | null; + @state() @storage({ storage: "sessionStorage", key: "script-table-search", @@ -146,6 +147,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { }) private _filter = ""; + @state() @storage({ storage: "sessionStorage", key: "script-table-filters-full", diff --git a/src/panels/config/tags/ha-config-tags.ts b/src/panels/config/tags/ha-config-tags.ts index 6bbcc4d76a..efa5370cb3 100644 --- a/src/panels/config/tags/ha-config-tags.ts +++ b/src/panels/config/tags/ha-config-tags.ts @@ -62,6 +62,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) { return this.hass.auth.external?.config.canWriteTag; } + @state() @storage({ storage: "sessionStorage", key: "tags-table-search", diff --git a/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts b/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts index 4118de6f14..46a5cbba49 100644 --- a/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts +++ b/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts @@ -76,6 +76,7 @@ export class VoiceAssistantsExpose extends LitElement { @state() private _extEntities?: Record; + @state() @storage({ storage: "sessionStorage", key: "voice-expose-table-search", diff --git a/src/panels/developer-tools/action/developer-tools-action.ts b/src/panels/developer-tools/action/developer-tools-action.ts index 979c653ffc..fd38101a02 100644 --- a/src/panels/developer-tools/action/developer-tools-action.ts +++ b/src/panels/developer-tools/action/developer-tools-action.ts @@ -52,6 +52,7 @@ class HaPanelDevAction extends LitElement { private _yamlValid = true; + @state() @storage({ key: "panel-dev-action-state-service-data", state: true, @@ -59,6 +60,7 @@ class HaPanelDevAction extends LitElement { }) private _serviceData?: ServiceAction = { action: "", target: {}, data: {} }; + @state() @storage({ key: "panel-dev-action-state-yaml-mode", state: true, diff --git a/src/panels/developer-tools/assist/developer-tools-assist.ts b/src/panels/developer-tools/assist/developer-tools-assist.ts index a10e169ea2..220c12300d 100644 --- a/src/panels/developer-tools/assist/developer-tools-assist.ts +++ b/src/panels/developer-tools/assist/developer-tools-assist.ts @@ -33,6 +33,7 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) { @state() supportedLanguages?: string[]; + @state() @storage({ key: "assist_debug_language", state: true, diff --git a/src/panels/developer-tools/state/developer-tools-state.ts b/src/panels/developer-tools/state/developer-tools-state.ts index 888442a12e..047b4aaaa2 100644 --- a/src/panels/developer-tools/state/developer-tools-state.ts +++ b/src/panels/developer-tools/state/developer-tools-state.ts @@ -62,6 +62,7 @@ class HaPanelDevState extends LitElement { @state() private _validJSON = true; + @state() @storage({ key: "devToolsShowAttributes", state: true, diff --git a/src/panels/history/ha-panel-history.ts b/src/panels/history/ha-panel-history.ts index cc59c9342f..b52f3eaf19 100644 --- a/src/panels/history/ha-panel-history.ts +++ b/src/panels/history/ha-panel-history.ts @@ -63,6 +63,7 @@ class HaPanelHistory extends LitElement { @state() private _endDate: Date; + @state() @storage({ key: "historyPickedValue", state: true, diff --git a/src/panels/logbook/ha-panel-logbook.ts b/src/panels/logbook/ha-panel-logbook.ts index df556370ba..037f5da57d 100644 --- a/src/panels/logbook/ha-panel-logbook.ts +++ b/src/panels/logbook/ha-panel-logbook.ts @@ -39,6 +39,7 @@ export class HaPanelLogbook extends LitElement { @state() private _showBack?: boolean; + @state() @storage({ key: "logbookPickedValue", state: true, diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts index 47e484d21c..46d39042ab 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts @@ -62,6 +62,7 @@ export class HuiEnergyDevicesDetailGraphCard @state() private _compareEnd?: Date; + @state() @storage({ key: "energy-devices-hidden-stats", state: true, diff --git a/src/panels/lovelace/cards/hui-button-card.ts b/src/panels/lovelace/cards/hui-button-card.ts index 068f8e3866..9b6dfcc812 100644 --- a/src/panels/lovelace/cards/hui-button-card.ts +++ b/src/panels/lovelace/cards/hui-button-card.ts @@ -86,6 +86,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard { @state() private _config?: ButtonCardConfig; + @state() @consume({ context: statesContext, subscribe: true }) @transform({ transformer: function (this: HuiButtonCard, value: HassEntities) { @@ -111,6 +112,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard { @consume({ context: configContext, subscribe: true }) _hassConfig!: HassConfig; + @state() @consume({ context: entitiesContext, subscribe: true }) @transform({ transformer: function (this: HuiButtonCard, value) { diff --git a/src/panels/lovelace/editor/badge-editor/hui-badge-picker.ts b/src/panels/lovelace/editor/badge-editor/hui-badge-picker.ts index 1e720a4632..af0030c52c 100644 --- a/src/panels/lovelace/editor/badge-editor/hui-badge-picker.ts +++ b/src/panels/lovelace/editor/badge-editor/hui-badge-picker.ts @@ -43,6 +43,7 @@ export class HuiBadgePicker extends LitElement { @property({ attribute: false }) public suggestedBadges?: string[]; + @state() @storage({ key: "dashboardBadgeClipboard", state: true, diff --git a/src/panels/lovelace/editor/card-editor/hui-card-picker.ts b/src/panels/lovelace/editor/card-editor/hui-card-picker.ts index edb1c704fd..f663f36021 100644 --- a/src/panels/lovelace/editor/card-editor/hui-card-picker.ts +++ b/src/panels/lovelace/editor/card-editor/hui-card-picker.ts @@ -42,6 +42,7 @@ export class HuiCardPicker extends LitElement { @property({ attribute: false }) public suggestedCards?: string[]; + @state() @storage({ key: "dashboardCardClipboard", state: true, diff --git a/src/panels/media-browser/ha-panel-media-browser.ts b/src/panels/media-browser/ha-panel-media-browser.ts index 05b2799ae5..d84f09a703 100644 --- a/src/panels/media-browser/ha-panel-media-browser.ts +++ b/src/panels/media-browser/ha-panel-media-browser.ts @@ -64,6 +64,7 @@ class PanelMediaBrowser extends LitElement { @state() _currentItem?: MediaPlayerItem; + @state() @storage({ key: "mediaBrowserPreferredLayout", state: true, @@ -78,6 +79,7 @@ class PanelMediaBrowser extends LitElement { }, ]; + @state() @storage({ key: "mediaBrowseEntityId", state: true, diff --git a/src/panels/todo/ha-panel-todo.ts b/src/panels/todo/ha-panel-todo.ts index d6c2369822..9dcb961478 100644 --- a/src/panels/todo/ha-panel-todo.ts +++ b/src/panels/todo/ha-panel-todo.ts @@ -9,7 +9,7 @@ import { } from "@mdi/js"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { LitElement, css, html, nothing } from "lit"; -import { customElement, property } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { storage } from "../../common/decorators/storage"; @@ -55,6 +55,7 @@ class PanelTodo extends LitElement { @property({ type: Boolean, reflect: true }) public mobile = false; + @state() @storage({ key: "selectedTodoEntity", state: true, From 1154d1769d016795c86fd7bdece3bcc080d3b1d8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 2 May 2025 21:34:23 +0300 Subject: [PATCH 25/80] Bumped version to 20250502.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cb3fd4db2f..7f9b269a72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20250430.2" +version = "20250502.0" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend" From 74488c0b96cc25cb7d6966c4a1cd6c5101317fe7 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 5 May 2025 11:12:43 +0200 Subject: [PATCH 26/80] Use new entity picker style in quick bar (#25265) * Use new entity picker style in quick bar * Cleanup * Add missing no area * Process code review --- src/dialogs/quick-bar/ha-quick-bar.ts | 162 ++++++++++++++++++++------ 1 file changed, 124 insertions(+), 38 deletions(-) diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index 828397f353..cf57a14e31 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -15,24 +15,26 @@ import { customElement, property, query, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; +import Fuse from "fuse.js"; import { canShowPage } from "../../common/config/can_show_page"; import { componentsWithService } from "../../common/config/components_with_service"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { fireEvent } from "../../common/dom/fire_event"; -import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name"; -import { computeStateName } from "../../common/entity/compute_state_name"; +import { + computeDeviceName, + computeDeviceNameDisplay, +} from "../../common/entity/compute_device_name"; import { navigate } from "../../common/navigate"; import { caseInsensitiveStringCompare } from "../../common/string/compare"; import type { ScorableTextItem } from "../../common/string/filter/sequence-matching"; -import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching"; import { debounce } from "../../common/util/debounce"; import "../../components/ha-icon-button"; import "../../components/ha-label"; import "../../components/ha-list"; -import "../../components/ha-list-item"; import "../../components/ha-spinner"; import "../../components/ha-textfield"; import "../../components/ha-tip"; +import "../../components/ha-md-list-item"; import { fetchHassioAddonsInfo } from "../../data/hassio/addon"; import { domainToName } from "../../data/integration"; import { getPanelNameTranslationKey } from "../../data/panel"; @@ -44,6 +46,13 @@ import type { HomeAssistant } from "../../types"; import { showConfirmationDialog } from "../generic/show-dialog-box"; import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog"; import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar"; +import { getEntityContext } from "../../common/entity/context/get_entity_context"; +import { computeEntityName } from "../../common/entity/compute_entity_name"; +import { computeAreaName } from "../../common/entity/compute_area_name"; +import { computeRTL } from "../../common/util/compute_rtl"; +import { computeDomain } from "../../common/entity/compute_domain"; +import { computeStateName } from "../../common/entity/compute_state_name"; +import { HaFuse } from "../../resources/fuse"; interface QuickBarItem extends ScorableTextItem { primaryText: string; @@ -59,6 +68,9 @@ interface CommandItem extends QuickBarItem { interface EntityItem extends QuickBarItem { altText: string; icon?: TemplateResult; + translatedDomain: string; + entityId: string; + friendlyName: string; } interface DeviceItem extends QuickBarItem { @@ -82,6 +94,23 @@ type BaseNavigationCommand = Pick< QuickBarNavigationItem, "primaryText" | "path" >; + +const DOMAIN_STYLE = styleMap({ + fontSize: "var(--ha-font-size-s)", + fontWeight: "var(--ha-font-weight-normal)", + lineHeight: "var(--ha-line-height-normal)", + alignSelf: "flex-end", + maxWidth: "30%", + textOverflow: "ellipsis", + overflow: "hidden", + whiteSpace: "nowrap", +}); + +const ENTITY_ID_STYLE = styleMap({ + fontFamily: "var(--ha-font-family-code)", + fontSize: "var(--ha-font-size-xs)", +}); + @customElement("ha-quick-bar") export class QuickBar extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -139,6 +168,11 @@ export class QuickBar extends LitElement { } } + protected firstUpdated(changedProps) { + super.firstUpdated(changedProps); + this.hass.loadBackendTranslation("title"); + } + private _getItems = memoizeOne( ( mode: QuickBarMode, @@ -323,61 +357,65 @@ export class QuickBar extends LitElement { private _renderDeviceItem(item: DeviceItem, index?: number) { return html` - - ${item.primaryText} + ${item.primaryText} ${item.area - ? html` - ${item.area} - ` + ? html` ${item.area} ` : nothing} - + `; } private _renderEntityItem(item: EntityItem, index?: number) { + const showEntityId = this.hass.userData?.showEntityIdPicker; + return html` - ${item.iconPath ? html` ` - : html`${item.icon}`} - ${item.primaryText} + : html`${item.icon}`} + ${item.primaryText} ${item.altText - ? html` - ${item.altText} - ` + ? html` ${item.altText} ` : nothing} - + ${item.entityId && showEntityId + ? html`${item.entityId}` + : nothing} + ${item.translatedDomain && !showEntityId + ? html`
    + ${item.translatedDomain} +
    ` + : nothing} + `; } private _renderCommandItem(item: CommandItem, index?: number) { return html` - ${item.iconPath ? html` - + ` : nothing} ${item.categoryText} @@ -394,7 +435,7 @@ export class QuickBar extends LitElement { ${item.primaryText} - + `; } @@ -421,7 +462,7 @@ export class QuickBar extends LitElement { } private _getItemAtIndex(index: number): ListItem | null { - return this.renderRoot.querySelector(`ha-list-item[index="${index}"]`); + return this.renderRoot.querySelector(`ha-md-list-item[index="${index}"]`); } private _addSpinnerToCommandItem(index: number): void { @@ -519,7 +560,7 @@ export class QuickBar extends LitElement { } private _handleItemClick(ev) { - const listItem = ev.target.closest("ha-list-item"); + const listItem = ev.target.closest("ha-md-list-item"); this._processItemAndCloseDialog( listItem.item, Number(listItem.getAttribute("index")) @@ -555,18 +596,43 @@ export class QuickBar extends LitElement { } private _generateEntityItems(): EntityItem[] { + const isRTL = computeRTL(this.hass); + return Object.keys(this.hass.states) .map((entityId) => { - const entityState = this.hass.states[entityId]; + const stateObj = this.hass.states[entityId]; + + const { area, device } = getEntityContext(stateObj, this.hass); + + const friendlyName = computeStateName(stateObj); // Keep this for search + const entityName = computeEntityName(stateObj, this.hass); + const deviceName = device ? computeDeviceName(device) : undefined; + const areaName = area ? computeAreaName(area) : undefined; + + const primary = entityName || deviceName || entityId; + const secondary = [areaName, entityName ? deviceName : undefined] + .filter(Boolean) + .join(isRTL ? " ◂ " : " ▸ "); + + const translatedDomain = domainToName( + this.hass.localize, + computeDomain(entityId) + ); + const entityItem = { - primaryText: computeStateName(entityState), - altText: entityId, + primaryText: primary, + altText: + secondary || + this.hass.localize("ui.components.device-picker.no_area"), icon: html` `, + translatedDomain: translatedDomain, + entityId: entityId, + friendlyName: friendlyName, action: () => fireEvent(this, "hass-more-info", { entityId }), }; @@ -846,9 +912,30 @@ export class QuickBar extends LitElement { }); } + private _fuseIndex = memoizeOne((items: QuickBarItem[]) => + Fuse.createIndex( + [ + "primaryText", + "altText", + "friendlyName", + "translatedDomain", + "entityId", // for technical search + ], + items + ) + ); + private _filterItems = memoizeOne( - (items: QuickBarItem[], filter: string): QuickBarItem[] => - fuzzyFilterSort(filter.trimLeft(), items) + (items: QuickBarItem[], filter: string): QuickBarItem[] => { + const index = this._fuseIndex(items); + const fuse = new HaFuse(items, {}, index); + + const results = fuse.multiTermsSearch(filter.trim()); + if (!results || !results.length) { + return items; + } + return results.map((result) => result.item); + } ); static get styles() { @@ -930,9 +1017,8 @@ export class QuickBar extends LitElement { direction: var(--direction); } - ha-list-item { + ha-md-list-item { width: 100%; - --mdc-list-item-graphic-margin: 20px; } ha-tip { From cfc7f91f0313c58fc661e4cad1861ed88d76e8ef Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sun, 4 May 2025 14:41:01 +0300 Subject: [PATCH 27/80] Fix select entity row opening more info on select (#25292) --- src/panels/lovelace/entity-rows/hui-select-entity-row.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/panels/lovelace/entity-rows/hui-select-entity-row.ts b/src/panels/lovelace/entity-rows/hui-select-entity-row.ts index fa7fb03ca2..17c6dbf2d9 100644 --- a/src/panels/lovelace/entity-rows/hui-select-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-select-entity-row.ts @@ -93,6 +93,7 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow { `; private _handleAction(ev): void { + ev.stopPropagation(); const stateObj = this.hass!.states[this._config!.entity] as SelectEntity; const option = ev.target.value; From 55770f3e023059acfbc26895c0292224fb822f42 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sun, 4 May 2025 10:38:01 +0300 Subject: [PATCH 28/80] Fix display of disabled items in traces (#25293) --- src/components/trace/ha-timeline.ts | 6 +++--- src/components/trace/hat-graph-node.ts | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/trace/ha-timeline.ts b/src/components/trace/ha-timeline.ts index b7ab5b2757..4ee033573c 100644 --- a/src/components/trace/ha-timeline.ts +++ b/src/components/trace/ha-timeline.ts @@ -11,8 +11,8 @@ export class HaTimeline extends LitElement { @property({ type: Boolean, reflect: true }) public raised = false; - @property({ attribute: false, reflect: true, type: Boolean }) notEnabled = - false; + @property({ attribute: "not-enabled", reflect: true, type: Boolean }) + notEnabled = false; @property({ attribute: "last-item", type: Boolean }) public lastItem = false; @@ -82,7 +82,7 @@ export class HaTimeline extends LitElement { margin-inline-start: initial; width: 24px; } - :host([notEnabled]) ha-svg-icon { + :host([not-enabled]) ha-svg-icon { opacity: 0.5; } ha-svg-icon { diff --git a/src/components/trace/hat-graph-node.ts b/src/components/trace/hat-graph-node.ts index 330338b7cc..10ea69d58b 100644 --- a/src/components/trace/hat-graph-node.ts +++ b/src/components/trace/hat-graph-node.ts @@ -17,8 +17,8 @@ export class HatGraphNode extends LitElement { @property({ type: Boolean }) public error = false; - @property({ attribute: false, reflect: true, type: Boolean }) notEnabled = - false; + @property({ attribute: "not-enabled", reflect: true, type: Boolean }) + notEnabled = false; @property({ attribute: "graph-start", reflect: true, type: Boolean }) graphStart = false; @@ -127,13 +127,13 @@ export class HatGraphNode extends LitElement { --stroke-clr: var(--hover-clr); --icon-clr: var(--default-icon-clr); } - :host([notEnabled]) circle { + :host([not-enabled]) circle { --stroke-clr: var(--disabled-clr); } - :host([notEnabled][active]) circle { + :host([not-enabled][active]) circle { --stroke-clr: var(--disabled-active-clr); } - :host([notEnabled]:hover) circle { + :host([not-enabled]:hover) circle { --stroke-clr: var(--disabled-hover-clr); } svg:not(.safari) { From f6e36f203854b2df8c7605c8a03dbc236247b295 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 5 May 2025 01:33:52 -0400 Subject: [PATCH 29/80] Add profile security link to My Home Assistant (#25303) --- src/panels/my/ha-panel-my.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/panels/my/ha-panel-my.ts b/src/panels/my/ha-panel-my.ts index cd78c52e55..c308cc32e4 100644 --- a/src/panels/my/ha-panel-my.ts +++ b/src/panels/my/ha-panel-my.ts @@ -255,6 +255,9 @@ export const getMyRedirects = (): Redirects => ({ profile: { redirect: "/profile", }, + profile_security: { + redirect: "/profile/security", + }, logbook: { component: "logbook", redirect: "/logbook", From 75e9ac9e7351f8e153f04dfbbd443b331a4dde26 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Mon, 5 May 2025 09:40:54 +0200 Subject: [PATCH 30/80] Fix flow-form header (#25305) --- src/dialogs/config-flow/step-flow-form.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/dialogs/config-flow/step-flow-form.ts b/src/dialogs/config-flow/step-flow-form.ts index 90b301b98a..0c6da5eda2 100644 --- a/src/dialogs/config-flow/step-flow-form.ts +++ b/src/dialogs/config-flow/step-flow-form.ts @@ -278,10 +278,6 @@ class StepFlowForm extends LitElement { } h2 { word-break: break-word; - padding-right: 72px; - padding-inline-end: 72px; - padding-inline-start: initial; - direction: var(--direction); } `, ]; From 868daf692dbdbc76f3eb5f719211bf19e224476d Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Mon, 5 May 2025 10:01:35 +0200 Subject: [PATCH 31/80] Use md-select for entity-row and state-card (#25307) --- .../hui-input-select-entity-row.ts | 20 +++++++------- src/state-summary/state-card-input_select.ts | 21 +++++++-------- src/state-summary/state-card-select.ts | 27 +++++++++---------- 3 files changed, 31 insertions(+), 37 deletions(-) diff --git a/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts b/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts index 99fa0a443d..d5bf01ae11 100644 --- a/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts @@ -3,8 +3,8 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { stopPropagation } from "../../../common/dom/stop_propagation"; import { computeStateName } from "../../../common/entity/compute_state_name"; -import "../../../components/ha-list-item"; -import "../../../components/ha-select"; +import "../../../components/ha-md-select"; +import "../../../components/ha-md-select-option"; import { UNAVAILABLE } from "../../../data/entity"; import { forwardHaptic } from "../../../data/haptics"; import type { InputSelectEntity } from "../../../data/input_select"; @@ -57,25 +57,26 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow { .config=${this._config} hide-name > - ${stateObj.attributes.options ? stateObj.attributes.options.map( (option) => - html`${option}` + html` +
    ${option}
    +
    ` ) - : ""} -
    + : nothing} + `; } @@ -85,9 +86,8 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow { display: flex; align-items: center; } - ha-select { + ha-md-select { width: 100%; - --ha-select-min-width: 0; } `; diff --git a/src/state-summary/state-card-input_select.ts b/src/state-summary/state-card-input_select.ts index d08fdccc1f..9f6cbbd112 100644 --- a/src/state-summary/state-card-input_select.ts +++ b/src/state-summary/state-card-input_select.ts @@ -1,11 +1,10 @@ import type { TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import { stopPropagation } from "../common/dom/stop_propagation"; import { computeStateName } from "../common/entity/compute_state_name"; import "../components/entity/state-badge"; -import "../components/ha-list-item"; -import "../components/ha-select"; +import "../components/ha-md-select"; +import "../components/ha-md-select-option"; import { UNAVAILABLE } from "../data/entity"; import type { InputSelectEntity } from "../data/input_select"; import { setInputSelectOption } from "../data/input_select"; @@ -20,23 +19,21 @@ class StateCardInputSelect extends LitElement { protected render(): TemplateResult { return html` - ${this.stateObj.attributes.options.map( (option) => - html`${option}` + html` +
    ${option}
    +
    ` )} -
    + `; } @@ -58,7 +55,7 @@ class StateCardInputSelect extends LitElement { margin-top: 10px; } - ha-select { + ha-md-select { width: 100%; } `; diff --git a/src/state-summary/state-card-select.ts b/src/state-summary/state-card-select.ts index 9027a17100..372cfb8c5d 100644 --- a/src/state-summary/state-card-select.ts +++ b/src/state-summary/state-card-select.ts @@ -1,11 +1,10 @@ import type { TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import { stopPropagation } from "../common/dom/stop_propagation"; import { computeStateName } from "../common/entity/compute_state_name"; import "../components/entity/state-badge"; -import "../components/ha-list-item"; -import "../components/ha-select"; +import "../components/ha-md-select"; +import "../components/ha-md-select-option"; import { UNAVAILABLE } from "../data/entity"; import type { SelectEntity } from "../data/select"; import { setSelectOption } from "../data/select"; @@ -20,24 +19,22 @@ class StateCardSelect extends LitElement { protected render(): TemplateResult { return html` - ${this.stateObj.attributes.options.map( (option) => html` - - ${this.hass.formatEntityState(this.stateObj, option)} - + +
    + ${this.hass.formatEntityState(this.stateObj, option)} +
    +
    ` )} -
    + `; } @@ -59,7 +56,7 @@ class StateCardSelect extends LitElement { margin-top: 10px; } - ha-select { + ha-md-select { width: 100%; } `; From 3121721ac75ac973b71b69e91d204512e6bfd836 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Mon, 5 May 2025 10:57:50 +0200 Subject: [PATCH 32/80] Revert "Use md-select for entity-row and state-card" (#25308) Revert "Use md-select for entity-row and state-card (#25307)" This reverts commit 3c9dce20e2679bb14d9dd6caecc38e47ab919209. --- .../hui-input-select-entity-row.ts | 20 +++++++------- src/state-summary/state-card-input_select.ts | 21 ++++++++------- src/state-summary/state-card-select.ts | 27 ++++++++++--------- 3 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts b/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts index d5bf01ae11..99fa0a443d 100644 --- a/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts @@ -3,8 +3,8 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { stopPropagation } from "../../../common/dom/stop_propagation"; import { computeStateName } from "../../../common/entity/compute_state_name"; -import "../../../components/ha-md-select"; -import "../../../components/ha-md-select-option"; +import "../../../components/ha-list-item"; +import "../../../components/ha-select"; import { UNAVAILABLE } from "../../../data/entity"; import { forwardHaptic } from "../../../data/haptics"; import type { InputSelectEntity } from "../../../data/input_select"; @@ -57,26 +57,25 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow { .config=${this._config} hide-name > - ${stateObj.attributes.options ? stateObj.attributes.options.map( (option) => - html` -
    ${option}
    -
    ` + html`${option}` ) - : nothing} -
    + : ""} + `; } @@ -86,8 +85,9 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow { display: flex; align-items: center; } - ha-md-select { + ha-select { width: 100%; + --ha-select-min-width: 0; } `; diff --git a/src/state-summary/state-card-input_select.ts b/src/state-summary/state-card-input_select.ts index 9f6cbbd112..d08fdccc1f 100644 --- a/src/state-summary/state-card-input_select.ts +++ b/src/state-summary/state-card-input_select.ts @@ -1,10 +1,11 @@ import type { TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; +import { stopPropagation } from "../common/dom/stop_propagation"; import { computeStateName } from "../common/entity/compute_state_name"; import "../components/entity/state-badge"; -import "../components/ha-md-select"; -import "../components/ha-md-select-option"; +import "../components/ha-list-item"; +import "../components/ha-select"; import { UNAVAILABLE } from "../data/entity"; import type { InputSelectEntity } from "../data/input_select"; import { setInputSelectOption } from "../data/input_select"; @@ -19,21 +20,23 @@ class StateCardInputSelect extends LitElement { protected render(): TemplateResult { return html` - ${this.stateObj.attributes.options.map( (option) => - html` -
    ${option}
    -
    ` + html`${option}` )} -
    + `; } @@ -55,7 +58,7 @@ class StateCardInputSelect extends LitElement { margin-top: 10px; } - ha-md-select { + ha-select { width: 100%; } `; diff --git a/src/state-summary/state-card-select.ts b/src/state-summary/state-card-select.ts index 372cfb8c5d..9027a17100 100644 --- a/src/state-summary/state-card-select.ts +++ b/src/state-summary/state-card-select.ts @@ -1,10 +1,11 @@ import type { TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; +import { stopPropagation } from "../common/dom/stop_propagation"; import { computeStateName } from "../common/entity/compute_state_name"; import "../components/entity/state-badge"; -import "../components/ha-md-select"; -import "../components/ha-md-select-option"; +import "../components/ha-list-item"; +import "../components/ha-select"; import { UNAVAILABLE } from "../data/entity"; import type { SelectEntity } from "../data/select"; import { setSelectOption } from "../data/select"; @@ -19,22 +20,24 @@ class StateCardSelect extends LitElement { protected render(): TemplateResult { return html` - ${this.stateObj.attributes.options.map( (option) => html` - -
    - ${this.hass.formatEntityState(this.stateObj, option)} -
    -
    + + ${this.hass.formatEntityState(this.stateObj, option)} + ` )} -
    + `; } @@ -56,7 +59,7 @@ class StateCardSelect extends LitElement { margin-top: 10px; } - ha-md-select { + ha-select { width: 100%; } `; From 5cd68301ed46243dfdbe8369c5d96d570e6bfd1a Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 5 May 2025 11:42:21 +0200 Subject: [PATCH 33/80] Fix pasting yaml in automation code editor (#25309) Fix pasting yaml in code editor --- src/common/dom/can-override-input.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/common/dom/can-override-input.ts b/src/common/dom/can-override-input.ts index d0896ba0a6..06aa28a8b0 100644 --- a/src/common/dom/can-override-input.ts +++ b/src/common/dom/can-override-input.ts @@ -1,5 +1,11 @@ export const canOverrideAlphanumericInput = (composedPath: EventTarget[]) => { - if (composedPath.some((el) => "tagName" in el && el.tagName === "HA-MENU")) { + if ( + composedPath.some( + (el) => + "tagName" in el && + (el.tagName === "HA-MENU" || el.tagName === "HA-CODE-EDITOR") + ) + ) { return false; } From 376cac60027f8848c59104b4faa2ce4c59061eec Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Mon, 5 May 2025 11:13:50 +0200 Subject: [PATCH 34/80] Fix select entity change (#25310) --- src/panels/lovelace/components/hui-generic-entity-row.ts | 1 + src/panels/lovelace/entity-rows/hui-select-entity-row.ts | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/lovelace/components/hui-generic-entity-row.ts b/src/panels/lovelace/components/hui-generic-entity-row.ts index 1ae552452f..26c51b773b 100644 --- a/src/panels/lovelace/components/hui-generic-entity-row.ts +++ b/src/panels/lovelace/components/hui-generic-entity-row.ts @@ -163,6 +163,7 @@ export class HuiGenericEntityRow extends LitElement { @touchend=${stopPropagation} @keydown=${stopPropagation} @click=${stopPropagation} + @action=${stopPropagation} >`}
`; diff --git a/src/panels/lovelace/entity-rows/hui-select-entity-row.ts b/src/panels/lovelace/entity-rows/hui-select-entity-row.ts index 17c6dbf2d9..fa7fb03ca2 100644 --- a/src/panels/lovelace/entity-rows/hui-select-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-select-entity-row.ts @@ -93,7 +93,6 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow { `; private _handleAction(ev): void { - ev.stopPropagation(); const stateObj = this.hass!.states[this._config!.entity] as SelectEntity; const option = ev.target.value; From 3e053e07c6762ad00d2a87d8373009be5ece2649 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Mon, 5 May 2025 13:11:42 +0200 Subject: [PATCH 35/80] Fix selected entity overflow (#25311) --- src/panels/lovelace/editor/hui-entities-card-row-editor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/panels/lovelace/editor/hui-entities-card-row-editor.ts b/src/panels/lovelace/editor/hui-entities-card-row-editor.ts index 06d9ab0f35..ab574af88d 100644 --- a/src/panels/lovelace/editor/hui-entities-card-row-editor.ts +++ b/src/panels/lovelace/editor/hui-entities-card-row-editor.ts @@ -210,6 +210,7 @@ export class HuiEntitiesCardRowEditor extends LitElement { .entity ha-entity-picker { flex-grow: 1; + min-width: 0; } .special-row { From 70fef59401ffc3466075ff1e3fb3faae491b099e Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Mon, 5 May 2025 19:20:22 +0200 Subject: [PATCH 36/80] Fix options and repair flow success (#25312) --- .../config-flow/step-flow-create-entry.ts | 129 +++++++++--------- 1 file changed, 66 insertions(+), 63 deletions(-) diff --git a/src/dialogs/config-flow/step-flow-create-entry.ts b/src/dialogs/config-flow/step-flow-create-entry.ts index da5ab1d183..6f59bf593d 100644 --- a/src/dialogs/config-flow/step-flow-create-entry.ts +++ b/src/dialogs/config-flow/step-flow-create-entry.ts @@ -129,70 +129,73 @@ class StepFlowCreateEntry extends LitElement { )}` : nothing} - ${devices.length === 0 - ? html`

- ${localize( - "ui.panel.config.integrations.config_flow.created_config", - { name: this.step.title } - )} -

` - : html` -
- ${devices.map( - (device) => html` -
-
- ${this.step.result?.domain - ? html`${domainToName(` - : nothing} -
- ${device.model || device.manufacturer} - ${device.model - ? html` - ${device.manufacturer} - ` - : nothing} -
-
- - -
- ` + ${devices.length === 0 && + ["options_flow", "repair_flow"].includes(this.flowConfig.flowType) + ? nothing + : devices.length === 0 + ? html`

+ ${localize( + "ui.panel.config.integrations.config_flow.created_config", + { name: this.step.title } )} -

- `} +

` + : html` +
+ ${devices.map( + (device) => html` +
+
+ ${this.step.result?.domain + ? html`${domainToName(` + : nothing} +
+ ${device.model || device.manufacturer} + ${device.model + ? html` + ${device.manufacturer} + ` + : nothing} +
+
+ + +
+ ` + )} +
+ `}
Date: Mon, 5 May 2025 15:05:35 +0200 Subject: [PATCH 37/80] Fix zwave add device LR/mesh icons (#25313) Fix zwave LR/mesh icons --- .../images/z-wave-add-node/long-range.svg | 10 +++---- public/static/images/z-wave-add-node/mesh.svg | 30 +++++++++---------- .../images/z-wave-add-node/mesh_dark.svg | 29 +++++++++--------- 3 files changed, 34 insertions(+), 35 deletions(-) diff --git a/public/static/images/z-wave-add-node/long-range.svg b/public/static/images/z-wave-add-node/long-range.svg index 48deddc513..32fa115cc8 100644 --- a/public/static/images/z-wave-add-node/long-range.svg +++ b/public/static/images/z-wave-add-node/long-range.svg @@ -1,13 +1,13 @@ - + - + - - + + - + diff --git a/public/static/images/z-wave-add-node/mesh.svg b/public/static/images/z-wave-add-node/mesh.svg index 92a03c444a..48fba567f4 100644 --- a/public/static/images/z-wave-add-node/mesh.svg +++ b/public/static/images/z-wave-add-node/mesh.svg @@ -1,19 +1,19 @@ - - - - - - - - + + + + + + + + - - + + - - - - - + + + + + diff --git a/public/static/images/z-wave-add-node/mesh_dark.svg b/public/static/images/z-wave-add-node/mesh_dark.svg index 1824489e47..22cda5e4f1 100644 --- a/public/static/images/z-wave-add-node/mesh_dark.svg +++ b/public/static/images/z-wave-add-node/mesh_dark.svg @@ -1,19 +1,18 @@ - - - - - - - - + + + + + + + - - + + - - - - - + + + + + From c178488aacdc51e1e8bb01f8782773e02dfaf64f Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Mon, 5 May 2025 10:09:16 -0700 Subject: [PATCH 38/80] Add energy hourly calculations to CSV report (#25315) --- src/panels/energy/ha-panel-energy.ts | 120 +++++++++++++++++++++++++-- 1 file changed, 113 insertions(+), 7 deletions(-) diff --git a/src/panels/energy/ha-panel-energy.ts b/src/panels/energy/ha-panel-energy.ts index 7bf69dbc23..cb63510b66 100644 --- a/src/panels/energy/ha-panel-energy.ts +++ b/src/panels/energy/ha-panel-energy.ts @@ -22,11 +22,14 @@ import type { DeviceConsumptionEnergyPreference, } from "../../data/energy"; import { + computeConsumptionData, getEnergyDataCollection, getEnergyGasUnit, getEnergyWaterUnit, + getSummedData, } from "../../data/energy"; import { fileDownload } from "../../util/file_download"; +import type { StatisticValue } from "../../data/recorder"; const ENERGY_LOVELACE_CONFIG: LovelaceConfig = { views: [ @@ -177,18 +180,20 @@ class PanelEnergy extends LitElement { const csv: string[] = []; csv[0] = headers; - const processStat = function (stat: string, type: string, unit: string) { + const processCsvRow = function ( + id: string, + type: string, + unit: string, + data: StatisticValue[] + ) { let n = 0; const row: string[] = []; - if (!stats[stat]) { - return; - } - row.push(stat); + row.push(id); row.push(type); row.push(unit.normalize("NFKD")); times.forEach((t) => { - if (n < stats[stat].length && stats[stat][n].start === t) { - row.push((stats[stat][n].change ?? "").toString()); + if (n < data.length && data[n].start === t) { + row.push((data[n].change ?? "").toString()); n++; } else { row.push(""); @@ -197,6 +202,14 @@ class PanelEnergy extends LitElement { csv.push(row.join(",") + "\n"); }; + const processStat = function (stat: string, type: string, unit: string) { + if (!stats[stat]) { + return; + } + + processCsvRow(stat, type, unit, stats[stat]); + }; + const currency = this.hass.config.currency; const printCategory = function ( @@ -335,6 +348,99 @@ class PanelEnergy extends LitElement { printCategory("device_consumption", devices, electricUnit); + const { summedData, compareSummedData: _ } = getSummedData( + energyData.state + ); + const { consumption, compareConsumption: __ } = computeConsumptionData( + summedData, + undefined + ); + + const processConsumptionData = function ( + type: string, + unit: string, + data: Record + ) { + const data2: StatisticValue[] = []; + + Object.entries(data).forEach(([t, value]) => { + data2.push({ + start: Number(t), + end: NaN, + change: value, + }); + }); + + processCsvRow("", type, unit, data2); + }; + + const hasSolar = !!solar_productions.length; + const hasBattery = !!battery_ins.length; + const hasGridReturn = !!grid_productions.length; + const hasGridSource = !!grid_consumptions.length; + + if (hasGridSource) { + processConsumptionData( + "calculated_consumed_grid", + electricUnit, + consumption.used_grid + ); + if (hasBattery) { + processConsumptionData( + "calculated_grid_to_battery", + electricUnit, + consumption.grid_to_battery + ); + } + } + if (hasGridReturn && hasBattery) { + processConsumptionData( + "calculated_battery_to_grid", + electricUnit, + consumption.battery_to_grid + ); + } + if (hasBattery) { + processConsumptionData( + "calculated_consumed_battery", + electricUnit, + consumption.used_battery + ); + } + + if (hasSolar) { + processConsumptionData( + "calculated_consumed_solar", + electricUnit, + consumption.used_solar + ); + if (hasBattery) { + processConsumptionData( + "calculated_solar_to_battery", + electricUnit, + consumption.solar_to_battery + ); + } + if (hasGridReturn) { + processConsumptionData( + "calculated_solar_to_grid", + electricUnit, + consumption.solar_to_grid + ); + } + } + + if ( + (hasGridSource ? 1 : 0) + (hasSolar ? 1 : 0) + (hasBattery ? 1 : 0) > + 1 + ) { + processConsumptionData( + "calculated_total_consumption", + electricUnit, + consumption.used_total + ); + } + const blob = new Blob(csv, { type: "text/csv", }); From 1191a095768b6797d1ea71315cf0bcfa669513b9 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 5 May 2025 19:37:47 +0200 Subject: [PATCH 39/80] Use new entity naming in card entity picker (#25316) --- .../badge-editor/hui-dialog-create-badge.ts | 19 -- .../card-editor/hui-dialog-create-card.ts | 19 -- .../card-editor/hui-entity-picker-table.ts | 255 +++++++++++++----- .../unused-entities/hui-unused-entities.ts | 25 +- 4 files changed, 195 insertions(+), 123 deletions(-) diff --git a/src/panels/lovelace/editor/badge-editor/hui-dialog-create-badge.ts b/src/panels/lovelace/editor/badge-editor/hui-dialog-create-badge.ts index d14959e4fb..9388295802 100644 --- a/src/panels/lovelace/editor/badge-editor/hui-dialog-create-badge.ts +++ b/src/panels/lovelace/editor/badge-editor/hui-dialog-create-badge.ts @@ -4,11 +4,7 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { cache } from "lit/directives/cache"; import { classMap } from "lit/directives/class-map"; -import memoize from "memoize-one"; import { fireEvent } from "../../../../common/dom/fire_event"; -import { computeDomain } from "../../../../common/entity/compute_domain"; -import { computeStateName } from "../../../../common/entity/compute_state_name"; -import type { DataTableRowData } from "../../../../components/data-table/ha-data-table"; import "../../../../components/ha-dialog"; import "../../../../components/ha-dialog-header"; import "../../../../components/sl-tab-group"; @@ -137,7 +133,6 @@ export class HuiCreateDialogBadge no-label-float .hass=${this.hass} .narrow=${true} - .entities=${this._allEntities(this.hass.states)} @selected-changed=${this._handleSelectedChanged} > ` @@ -276,20 +271,6 @@ export class HuiCreateDialogBadge this.closeDialog(); } - - private _allEntities = memoize((entities) => - Object.keys(entities).map((entity) => { - const stateObj = this.hass.states[entity]; - return { - icon: "", - entity_id: entity, - stateObj, - name: computeStateName(stateObj), - domain: computeDomain(entity), - last_changed: stateObj!.last_changed, - } as DataTableRowData; - }) - ); } declare global { diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-create-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-create-card.ts index 314029fa0d..46927da9c8 100644 --- a/src/panels/lovelace/editor/card-editor/hui-dialog-create-card.ts +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-create-card.ts @@ -5,11 +5,7 @@ import { customElement, property, state } from "lit/decorators"; import { cache } from "lit/directives/cache"; import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; -import memoize from "memoize-one"; import { fireEvent } from "../../../../common/dom/fire_event"; -import { computeDomain } from "../../../../common/entity/compute_domain"; -import { computeStateName } from "../../../../common/entity/compute_state_name"; -import type { DataTableRowData } from "../../../../components/data-table/ha-data-table"; import "../../../../components/ha-dialog"; import "../../../../components/ha-dialog-header"; import "../../../../components/sl-tab-group"; @@ -157,7 +153,6 @@ export class HuiCreateDialogCard no-label-float .hass=${this.hass} narrow - .entities=${this._allEntities(this.hass.states)} @selected-changed=${this._handleSelectedChanged} > ` @@ -340,20 +335,6 @@ export class HuiCreateDialogCard this.closeDialog(); } - - private _allEntities = memoize((entities) => - Object.keys(entities).map((entity) => { - const stateObj = this.hass.states[entity]; - return { - icon: "", - entity_id: entity, - stateObj, - name: computeStateName(stateObj), - domain: computeDomain(entity), - last_changed: stateObj!.last_changed, - } as DataTableRowData; - }) - ); } declare global { diff --git a/src/panels/lovelace/editor/card-editor/hui-entity-picker-table.ts b/src/panels/lovelace/editor/card-editor/hui-entity-picker-table.ts index 5918aafef6..dd0c3a549c 100644 --- a/src/panels/lovelace/editor/card-editor/hui-entity-picker-table.ts +++ b/src/panels/lovelace/editor/card-editor/hui-entity-picker-table.ts @@ -1,9 +1,17 @@ -import type { TemplateResult } from "lit"; -import { css, html, LitElement } from "lit"; +import type { PropertyValues, TemplateResult } from "lit"; +import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; import type { HASSDomEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event"; +import { computeAreaName } from "../../../../common/entity/compute_area_name"; +import { computeDeviceName } from "../../../../common/entity/compute_device_name"; +import { computeDomain } from "../../../../common/entity/compute_domain"; +import { computeEntityName } from "../../../../common/entity/compute_entity_name"; +import { getEntityContext } from "../../../../common/entity/context/get_entity_context"; +import type { LocalizeFunc } from "../../../../common/translations/localize"; +import { computeRTL } from "../../../../common/util/compute_rtl"; import "../../../../components/data-table/ha-data-table"; import type { DataTableColumnContainer, @@ -12,8 +20,26 @@ import type { } from "../../../../components/data-table/ha-data-table"; import "../../../../components/entity/state-badge"; import "../../../../components/ha-relative-time"; +import { domainToName } from "../../../../data/integration"; import type { HomeAssistant } from "../../../../types"; +const ENTITY_ID_STYLE = styleMap({ + fontFamily: "var(--ha-font-family-code)", + fontSize: "var(--ha-font-size-xs)", +}); + +interface EntityPickerTableRowData extends DataTableRowData { + icon: string; + entity_id: string; + stateObj: any; + name: string; + entity_name?: string; + device_name?: string; + area_name?: string; + domain_name: string; + last_changed: string; +} + @customElement("hui-entity-picker-table") export class HuiEntityPickerTable extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -23,16 +49,69 @@ export class HuiEntityPickerTable extends LitElement { @property({ type: Boolean, attribute: "no-label-float" }) public noLabelFloat? = false; - @property({ type: Array }) public entities!: DataTableRowData[]; + @property({ type: Array }) public entities?: string[]; + + protected firstUpdated(_changedProperties: PropertyValues): void { + super.firstUpdated(_changedProperties); + this.hass.loadBackendTranslation("title"); + } + + private _data = memoizeOne( + ( + states: HomeAssistant["states"], + localize: LocalizeFunc, + entities?: string[] + ): EntityPickerTableRowData[] => + (entities || Object.keys(states)).map( + (entity) => { + const stateObj = this.hass.states[entity]; + + const { area, device } = getEntityContext(stateObj, this.hass); + + const entityName = computeEntityName(stateObj, this.hass); + const deviceName = device ? computeDeviceName(device) : undefined; + const areaName = area ? computeAreaName(area) : undefined; + const name = [deviceName, entityName].filter(Boolean).join(" "); + const domain = computeDomain(entity); + + return { + icon: "", + entity_id: entity, + stateObj, + name: name, + entity_name: entityName, + device_name: deviceName, + area_name: areaName, + domain_name: domainToName(localize, domain), + last_changed: stateObj!.last_changed, + } satisfies EntityPickerTableRowData; + } + ) + ); protected render(): TemplateResult { + const data = this._data( + this.hass.states, + this.hass.localize, + this.entities + ); + + const showEntityId = Boolean(this.hass.userData?.showEntityIdPicker); + + const columns = this._columns( + this.narrow, + computeRTL(this.hass), + showEntityId + ); + return html` { - const columns: DataTableColumnContainer = { - icon: { - title: "", - label: this.hass!.localize( - "ui.panel.lovelace.unused_entities.state_icon" + private _columns = memoizeOne( + (narrow: boolean, isRTL: boolean, showEntityId: boolean) => { + const columns: DataTableColumnContainer = { + icon: { + title: "", + label: this.hass!.localize( + "ui.panel.lovelace.unused_entities.state_icon" + ), + type: "icon", + template: (entity) => html` + + `, + }, + name: { + title: this.hass!.localize( + "ui.panel.lovelace.unused_entities.entity" + ), + sortable: true, + filterable: true, + flex: 2, + main: true, + direction: "asc", + template: (entity: any) => { + const primary = + entity.entity_name || entity.device_name || entity.entity_id; + const secondary = [ + entity.area_name, + entity.entity_name ? entity.device_name : undefined, + ] + .filter(Boolean) + .join(isRTL ? " ◂ " : " ▸ "); + return html` +
+ ${primary} + ${secondary + ? html`
${secondary}
` + : nothing} + ${narrow && showEntityId + ? html` +
+ ${entity.entity_id} +
+ ` + : nothing} +
+ `; + }, + }, + }; + + columns.entity_name = { + title: "entity_name", + filterable: true, + hidden: true, + }; + + columns.device_name = { + title: "device_name", + filterable: true, + hidden: true, + }; + + columns.area_name = { + title: "area_name", + filterable: true, + hidden: true, + }; + + columns.entity_id = { + title: this.hass!.localize( + "ui.panel.lovelace.unused_entities.entity_id" ), - type: "icon", - template: (entity) => html` - - `, - }, - name: { - title: this.hass!.localize("ui.panel.lovelace.unused_entities.entity"), sortable: true, filterable: true, - flex: 2, - main: true, - direction: "asc", - template: (entity: any) => html` -
- ${entity.name} - ${narrow - ? html`
${entity.entity_id}
` - : ""} -
+ hidden: narrow || !showEntityId, + }; + + columns.domain_name = { + title: this.hass!.localize("ui.panel.lovelace.unused_entities.domain"), + sortable: true, + filterable: true, + hidden: narrow || showEntityId, + }; + + columns.last_changed = { + title: this.hass!.localize( + "ui.panel.lovelace.unused_entities.last_changed" + ), + type: "numeric", + sortable: true, + hidden: narrow, + template: (entity) => html` + `, - }, - }; + }; - columns.entity_id = { - title: this.hass!.localize("ui.panel.lovelace.unused_entities.entity_id"), - sortable: true, - filterable: true, - hidden: narrow, - }; - - columns.domain = { - title: this.hass!.localize("ui.panel.lovelace.unused_entities.domain"), - sortable: true, - filterable: true, - hidden: narrow, - }; - - columns.last_changed = { - title: this.hass!.localize( - "ui.panel.lovelace.unused_entities.last_changed" - ), - type: "numeric", - sortable: true, - hidden: narrow, - template: (entity) => html` - - `, - }; - - return columns; - }); + return columns; + } + ); private _handleSelectionChanged( ev: HASSDomEvent @@ -134,6 +254,9 @@ export class HuiEntityPickerTable extends LitElement { --data-table-border-width: 0; height: 100%; } + ha-data-table.show-entity-id { + --data-table-row-height: 64px; + } `; } diff --git a/src/panels/lovelace/editor/unused-entities/hui-unused-entities.ts b/src/panels/lovelace/editor/unused-entities/hui-unused-entities.ts index a63d3e3a50..392f439f73 100644 --- a/src/panels/lovelace/editor/unused-entities/hui-unused-entities.ts +++ b/src/panels/lovelace/editor/unused-entities/hui-unused-entities.ts @@ -3,22 +3,19 @@ import type { PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; -import { computeDomain } from "../../../../common/entity/compute_domain"; -import { computeStateName } from "../../../../common/entity/compute_state_name"; -import type { DataTableRowData } from "../../../../components/data-table/ha-data-table"; import "../../../../components/ha-fab"; import "../../../../components/ha-svg-icon"; +import type { LovelaceConfig } from "../../../../data/lovelace/config/types"; import type { HomeAssistant } from "../../../../types"; import { computeUnusedEntities } from "../../common/compute-unused-entities"; -import type { Lovelace } from "../../types"; -import "../card-editor/hui-entity-picker-table"; -import { showSuggestCardDialog } from "../card-editor/show-suggest-card-dialog"; -import { showSelectViewDialog } from "../select-view/show-select-view-dialog"; -import type { LovelaceConfig } from "../../../../data/lovelace/config/types"; import { computeCards, computeSection, } from "../../common/generate-lovelace-config"; +import type { Lovelace } from "../../types"; +import "../card-editor/hui-entity-picker-table"; +import { showSuggestCardDialog } from "../card-editor/show-suggest-card-dialog"; +import { showSelectViewDialog } from "../select-view/show-select-view-dialog"; @customElement("hui-unused-entities") export class HuiUnusedEntities extends LitElement { @@ -80,17 +77,7 @@ export class HuiUnusedEntities extends LitElement { { - const stateObj = this.hass!.states[entity]; - return { - icon: "", - entity_id: entity, - stateObj, - name: stateObj ? computeStateName(stateObj) : "Unavailable", - domain: computeDomain(entity), - last_changed: stateObj?.last_changed, - }; - }) as DataTableRowData[]} + .entities=${this._unusedEntities} @selected-changed=${this._handleSelectedChanged} >
From 44485c0de43bb7e0ab3744120aa73213723d1fe4 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 5 May 2025 18:56:16 +0200 Subject: [PATCH 40/80] Do not display no areas in entity pickers (#25317) --- src/components/entity/ha-entity-combo-box.ts | 4 +--- src/components/entity/ha-entity-picker.ts | 5 +---- src/dialogs/quick-bar/ha-quick-bar.ts | 23 +++++++++++++++++--- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/components/entity/ha-entity-combo-box.ts b/src/components/entity/ha-entity-combo-box.ts index 9a89fe23d3..f0f3ede62b 100644 --- a/src/components/entity/ha-entity-combo-box.ts +++ b/src/components/entity/ha-entity-combo-box.ts @@ -314,9 +314,7 @@ export class HaEntityComboBox extends LitElement { ...hass!.states[entityId], label: "", primary: primary, - secondary: - secondary || - this.hass.localize("ui.components.device-picker.no_area"), + secondary: secondary, translated_domain: translatedDomain, sorting_label: [deviceName, entityName].filter(Boolean).join("-"), entity_name: entityName || deviceName, diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index 0cba69c515..515a5a58b4 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -162,10 +162,7 @@ export class HaEntityPicker extends LitElement { slot="start" > ${primary} - - ${secondary || - this.hass.localize("ui.components.device-picker.no_area")} - + ${secondary} ${showClearIcon ? html` Date: Mon, 5 May 2025 12:54:33 -0400 Subject: [PATCH 41/80] Reorder my links (#25319) --- src/panels/my/ha-panel-my.ts | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/panels/my/ha-panel-my.ts b/src/panels/my/ha-panel-my.ts index c308cc32e4..d41ef36863 100644 --- a/src/panels/my/ha-panel-my.ts +++ b/src/panels/my/ha-panel-my.ts @@ -16,6 +16,11 @@ import "../../layouts/hass-error-screen"; import type { HomeAssistant, Route } from "../../types"; import { documentationUrl } from "../../util/documentation-url"; +// When a user presses "m", the user is redirected to the first redirect +// for which holds true currentPath.startsWith(redirect.redirect) +// That's why redirects should be sorted with more specific ones first +// Or else pressing "M" will link to the higher level page. + export const getMyRedirects = (): Redirects => ({ application_credentials: { redirect: "/config/application_credentials", @@ -73,15 +78,15 @@ export const getMyRedirects = (): Redirects => ({ brand: "string", }, }, - integrations: { - redirect: "/config/integrations", - }, integration: { redirect: "/config/integrations/integration", params: { domain: "string", }, }, + integrations: { + redirect: "/config/integrations", + }, config_mqtt: { component: "mqtt", redirect: "/config/mqtt", @@ -106,10 +111,6 @@ export const getMyRedirects = (): Redirects => ({ component: "matter", redirect: "/config/matter/add", }, - config_bluetooth: { - component: "bluetooth", - redirect: "/config/bluetooth", - }, bluetooth_advertisement_monitor: { component: "bluetooth", redirect: "/config/bluetooth/advertisement-monitor", @@ -118,6 +119,10 @@ export const getMyRedirects = (): Redirects => ({ component: "bluetooth", redirect: "/config/bluetooth/connection-monitor", }, + config_bluetooth: { + component: "bluetooth", + redirect: "/config/bluetooth", + }, config_dhcp: { component: "dhcp", redirect: "/config/dhcp", @@ -252,12 +257,12 @@ export const getMyRedirects = (): Redirects => ({ // customize was removed in 2021.12, fallback to dashboard redirect: "/config/dashboard", }, - profile: { - redirect: "/profile", - }, profile_security: { redirect: "/profile/security", }, + profile: { + redirect: "/profile", + }, logbook: { component: "logbook", redirect: "/logbook", @@ -270,10 +275,6 @@ export const getMyRedirects = (): Redirects => ({ component: "media_source", redirect: "/media-browser", }, - backup: { - component: "backup", - redirect: "/config/backup", - }, backup_list: { component: "backup", redirect: "/config/backup/backups", @@ -282,6 +283,10 @@ export const getMyRedirects = (): Redirects => ({ component: "backup", redirect: "/config/backup/settings", }, + backup: { + component: "backup", + redirect: "/config/backup", + }, supervisor_snapshots: { component: "backup", redirect: "/config/backup", From 92905c14331f482fac8fe50f4967c733355b920d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 5 May 2025 13:29:24 -0400 Subject: [PATCH 42/80] Clean up network browser nav (#25321) * Clean up network browser nav * Add ha-md-list --- .../config/network/ha-config-network-dhcp.ts | 66 ----------------- .../config/network/ha-config-network-ssdp.ts | 66 ----------------- .../network/ha-config-network-zeroconf.ts | 70 ------------------- .../network/ha-config-section-network.ts | 62 ++++++++++------ src/translations/en.json | 11 ++- 5 files changed, 47 insertions(+), 228 deletions(-) delete mode 100644 src/panels/config/network/ha-config-network-dhcp.ts delete mode 100644 src/panels/config/network/ha-config-network-ssdp.ts delete mode 100644 src/panels/config/network/ha-config-network-zeroconf.ts diff --git a/src/panels/config/network/ha-config-network-dhcp.ts b/src/panels/config/network/ha-config-network-dhcp.ts deleted file mode 100644 index ef70d11903..0000000000 --- a/src/panels/config/network/ha-config-network-dhcp.ts +++ /dev/null @@ -1,66 +0,0 @@ -import "@material/mwc-button/mwc-button"; -import type { CSSResultGroup } from "lit"; -import { css, html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators"; -import "../../../components/ha-button"; -import "../../../components/ha-card"; -import { haStyle } from "../../../resources/styles"; -import type { HomeAssistant } from "../../../types"; - -@customElement("ha-config-network-dhcp") -class ConfigNetworkDHCP extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - protected render() { - return html` - -
-

- ${this.hass.localize("ui.panel.config.network.discovery.dhcp_info")} -

-
-
- - `; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - ha-settings-row { - padding: 0; - } - - .card-actions { - display: flex; - flex-direction: row-reverse; - justify-content: space-between; - align-items: center; - } - `, // row-reverse so we tab first to "save" - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ha-config-network-dhcp": ConfigNetworkDHCP; - } -} diff --git a/src/panels/config/network/ha-config-network-ssdp.ts b/src/panels/config/network/ha-config-network-ssdp.ts deleted file mode 100644 index bd7bf83568..0000000000 --- a/src/panels/config/network/ha-config-network-ssdp.ts +++ /dev/null @@ -1,66 +0,0 @@ -import "@material/mwc-button/mwc-button"; -import type { CSSResultGroup } from "lit"; -import { css, html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators"; -import "../../../components/ha-button"; -import "../../../components/ha-card"; -import { haStyle } from "../../../resources/styles"; -import type { HomeAssistant } from "../../../types"; - -@customElement("ha-config-network-ssdp") -class ConfigNetworkSSDP extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - protected render() { - return html` - -
-

- ${this.hass.localize("ui.panel.config.network.discovery.ssdp_info")} -

-
- -
- `; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - ha-settings-row { - padding: 0; - } - - .card-actions { - display: flex; - flex-direction: row-reverse; - justify-content: space-between; - align-items: center; - } - `, // row-reverse so we tab first to "save" - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ha-config-network-ssdp": ConfigNetworkSSDP; - } -} diff --git a/src/panels/config/network/ha-config-network-zeroconf.ts b/src/panels/config/network/ha-config-network-zeroconf.ts deleted file mode 100644 index db97f0d019..0000000000 --- a/src/panels/config/network/ha-config-network-zeroconf.ts +++ /dev/null @@ -1,70 +0,0 @@ -import "@material/mwc-button/mwc-button"; -import type { CSSResultGroup } from "lit"; -import { css, html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators"; -import "../../../components/ha-button"; -import "../../../components/ha-card"; -import { haStyle } from "../../../resources/styles"; -import type { HomeAssistant } from "../../../types"; - -@customElement("ha-config-network-zeroconf") -class ConfigNetworkZeroconf extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - protected render() { - return html` - -
-

- ${this.hass.localize( - "ui.panel.config.network.discovery.zeroconf_info" - )} -

-
- -
- `; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - ha-settings-row { - padding: 0; - } - - .card-actions { - display: flex; - flex-direction: row-reverse; - justify-content: space-between; - align-items: center; - } - `, // row-reverse so we tab first to "save" - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ha-config-network-zeroconf": ConfigNetworkZeroconf; - } -} diff --git a/src/panels/config/network/ha-config-section-network.ts b/src/panels/config/network/ha-config-section-network.ts index c9a3df311d..567c18eb0f 100644 --- a/src/panels/config/network/ha-config-section-network.ts +++ b/src/panels/config/network/ha-config-section-network.ts @@ -3,15 +3,18 @@ import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import "../../../layouts/hass-subpage"; +import "../../../components/ha-card"; +import "../../../components/ha-md-list"; +import "../../../components/ha-md-list-item"; +import "../../../components/ha-icon-next"; import type { HomeAssistant, Route } from "../../../types"; import "./ha-config-network"; -import "./ha-config-network-dhcp"; -import "./ha-config-network-ssdp"; -import "./ha-config-network-zeroconf"; import "./ha-config-url-form"; import "./supervisor-hostname"; import "./supervisor-network"; +const NETWORK_BROWSERS = ["dhcp", "ssdp", "zeroconf"] as const; + @customElement("ha-config-section-network") class HaConfigSectionNetwork extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -38,20 +41,38 @@ class HaConfigSectionNetwork extends LitElement { : ""} - ${isComponentLoaded(this.hass, "dhcp") - ? html`` - : ""} - ${isComponentLoaded(this.hass, "ssdp") - ? html`` - : ""} - ${isComponentLoaded(this.hass, "zeroconf") - ? html`` + ${NETWORK_BROWSERS.some((component) => + isComponentLoaded(this.hass, component) + ) + ? html` + + + ${NETWORK_BROWSERS.map( + (domain) => html` + +
+ ${this.hass.localize( + `ui.panel.config.network.discovery.${domain}` + )} +
+
+ ${this.hass.localize( + `ui.panel.config.network.discovery.${domain}_info` + )} +
+ +
+ ` + )} +
+
+ ` : ""} @@ -68,14 +89,15 @@ class HaConfigSectionNetwork extends LitElement { supervisor-network, ha-config-url-form, ha-config-network, - ha-config-network-dhcp, - ha-config-network-ssdp, - ha-config-network-zeroconf { + .discovery-card { display: block; margin: 0 auto; margin-bottom: 24px; max-width: 600px; } + .discovery-card ha-md-list { + padding-top: 0; + } `; } diff --git a/src/translations/en.json b/src/translations/en.json index af02f16e77..4ceef74c9b 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -6399,15 +6399,14 @@ } }, "discovery": { + "title": "Network discovery", + "description": "Explore what data Home Assistant can see on the network.", "dhcp": "DHCP browser", - "dhcp_info": "The DHCP browser shows devices detected by Home Assistant using methods like DHCP, ARP+PTR lookups, and router-based device trackers. DHCP (Dynamic Host Configuration Protocol) data is received when devices join the network and request an IP address, allowing Home Assistant to discover them automatically. All devices found through these methods will appear here.", - "dhcp_browser": "View DHCP browser", + "dhcp_info": "Show devices detected by Home Assistant using methods like DHCP, ARP+PTR lookups, and router-based device trackers. DHCP (Dynamic Host Configuration Protocol) data is received when devices join the network and request an IP address.", "ssdp": "SSDP browser", - "ssdp_info": "The SSDP browser shows devices discovered by Home Assistant using SSDP/UPnP. Devices that Home Assistant has discovered will appear here.", - "ssdp_browser": "View SSDP browser", + "ssdp_info": "Show devices discovered by Home Assistant using SSDP/UPnP. Devices that Home Assistant has discovered will appear here.", "zeroconf": "Zeroconf browser", - "zeroconf_info": "The Zeroconf browser shows devices discovered by Home Assistant using mDNS. Only devices that Home Assistant is actively searching for will appear here.", - "zeroconf_browser": "View Zeroconf browser" + "zeroconf_info": "Show devices discovered by Home Assistant using mDNS. Only devices that Home Assistant is actively searching for will appear here." }, "network_adapter": "Network adapter", "network_adapter_info": "Configure which network adapters integrations will use. Currently this setting only affects multicast traffic. A restart is required for these settings to apply.", From 37671fd613c45800958f2304ac5854e8461f8680 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 5 May 2025 13:48:29 -0400 Subject: [PATCH 43/80] Populate integration domain My link (#25322) * Populate integration domain My link * break out of loop * Actually just return from function * Consolidate code --- src/state/quick-bar-mixin.ts | 60 +++++++++++++++++------------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/src/state/quick-bar-mixin.ts b/src/state/quick-bar-mixin.ts index 6e931a9bd7..f0effc7f03 100644 --- a/src/state/quick-bar-mixin.ts +++ b/src/state/quick-bar-mixin.ts @@ -16,6 +16,7 @@ import { extractSearchParamsObject } from "../common/url/search-params"; import { showVoiceCommandDialog } from "../dialogs/voice-command-dialog/show-ha-voice-command-dialog"; import { canOverrideAlphanumericInput } from "../common/dom/can-override-input"; import { showShortcutsDialog } from "../dialogs/shortcuts/show-shortcuts-dialog"; +import type { Redirects } from "../panels/my/ha-panel-my"; declare global { interface HASSDomEvents { @@ -143,49 +144,44 @@ export default >(superClass: T) => e.preventDefault(); const targetPath = mainWindow.location.pathname; - const isHassio = isComponentLoaded(this.hass, "hassio"); const myParams = new URLSearchParams(); - if (isHassio && targetPath.startsWith("/hassio")) { + let redirects: Redirects; + + if (targetPath.startsWith("/hassio")) { const myPanelSupervisor = await import( "../../hassio/src/hassio-my-redirect" ); - for (const [slug, redirect] of Object.entries( - myPanelSupervisor.REDIRECTS - )) { - if (targetPath.startsWith(redirect.redirect)) { - myParams.append("redirect", slug); - if (redirect.redirect === "/hassio/addon") { - myParams.append("addon", targetPath.split("/")[3]); - } - window.open( - `https://my.home-assistant.io/create-link/?${myParams.toString()}`, - "_blank" - ); - return; - } - } + redirects = myPanelSupervisor.REDIRECTS; + } else { + const myPanel = await import("../panels/my/ha-panel-my"); + redirects = myPanel.getMyRedirects(); } - const myPanel = await import("../panels/my/ha-panel-my"); + for (const [slug, redirect] of Object.entries(redirects)) { + if (!targetPath.startsWith(redirect.redirect)) { + continue; + } + myParams.append("redirect", slug); - for (const [slug, redirect] of Object.entries(myPanel.getMyRedirects())) { - if (targetPath.startsWith(redirect.redirect)) { - myParams.append("redirect", slug); - if (redirect.params) { - const params = extractSearchParamsObject(); - for (const key of Object.keys(redirect.params)) { - if (key in params) { - myParams.append(key, params[key]); - } + if (redirect.params) { + const params = extractSearchParamsObject(); + for (const key of Object.keys(redirect.params)) { + if (key in params) { + myParams.append(key, params[key]); } } - window.open( - `https://my.home-assistant.io/create-link/?${myParams.toString()}`, - "_blank" - ); - return; } + if (redirect.redirect === "/config/integrations/integration") { + myParams.append("domain", targetPath.split("/")[4]); + } else if (redirect.redirect === "/hassio/addon") { + myParams.append("addon", targetPath.split("/")[3]); + } + window.open( + `https://my.home-assistant.io/create-link/?${myParams.toString()}`, + "_blank" + ); + return; } showToast(this, { message: this.hass.localize( From b7f866faffbe81a621f0fefc17320ec0df45a734 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 5 May 2025 20:12:59 +0200 Subject: [PATCH 44/80] Bumped version to 20250502.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7f9b269a72..8d2173dca9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20250502.0" +version = "20250502.1" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend" From 78b2d17f104c43233bce1c3a6847678b0d6e6ef2 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Tue, 6 May 2025 13:50:23 +0200 Subject: [PATCH 45/80] Add custom retention info to backup locations (#25318) * Add retention messages to backup locations * Fix mobile * Use join --- .../config/ha-backup-config-agents.ts | 74 +++++++++++++++---- src/translations/en.json | 5 +- 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/src/panels/config/backup/components/config/ha-backup-config-agents.ts b/src/panels/config/backup/components/config/ha-backup-config-agents.ts index 687321f7c5..a9817dba96 100644 --- a/src/panels/config/backup/components/config/ha-backup-config-agents.ts +++ b/src/panels/config/backup/components/config/ha-backup-config-agents.ts @@ -1,5 +1,6 @@ import { mdiCog, mdiDelete, mdiHarddisk, mdiNas } from "@mdi/js"; -import { css, html, LitElement, nothing } from "lit"; +import { css, html, LitElement, nothing, type TemplateResult } from "lit"; +import { join } from "lit/directives/join"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../../common/dom/fire_event"; @@ -57,26 +58,51 @@ class HaBackupConfigAgents extends LitElement { ); } + const texts: (TemplateResult | string)[] = []; + + if (isNetworkMountAgent(agentId)) { + texts.push( + this.hass.localize( + "ui.panel.config.backup.agents.network_mount_agent_description" + ) + ); + } + const encryptionTurnedOff = this.agentsConfig?.[agentId]?.protected === false; if (encryptionTurnedOff) { - return html` - - - ${this.hass.localize( - "ui.panel.config.backup.agents.encryption_turned_off" - )} - - `; - } - - if (isNetworkMountAgent(agentId)) { - return this.hass.localize( - "ui.panel.config.backup.agents.network_mount_agent_description" + texts.push( + html`
+ + + ${this.hass.localize( + "ui.panel.config.backup.agents.encryption_turned_off" + )} + +
` ); } - return ""; + + const retention = this.agentsConfig?.[agentId]?.retention; + + if (retention) { + if (retention.copies === null && retention.days === null) { + texts.push( + this.hass.localize("ui.panel.config.backup.agents.retention_all") + ); + } else { + texts.push( + this.hass.localize( + `ui.panel.config.backup.agents.retention_${retention.copies ? "backups" : "days"}`, + { + count: retention.copies || retention.days, + } + ) + ); + } + } + return join(texts, html``); } private _availableAgents = memoizeOne( @@ -287,6 +313,11 @@ class HaBackupConfigAgents extends LitElement { gap: 8px; line-height: normal; } + .unencrypted-warning { + display: flex; + align-items: center; + gap: 4px; + } .dot { display: block; position: relative; @@ -294,11 +325,22 @@ class HaBackupConfigAgents extends LitElement { height: 8px; background-color: var(--disabled-color); border-radius: 50%; - flex: none; } .dot.warning { background-color: var(--warning-color); } + @media all and (max-width: 500px) { + .separator { + display: none; + } + ha-md-list-item [slot="supporting-text"] { + display: flex; + align-items: flex-start; + flex-direction: column; + justify-content: flex-start; + gap: 4px; + } + } `; } diff --git a/src/translations/en.json b/src/translations/en.json index 4ceef74c9b..10d0ce38df 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2489,7 +2489,10 @@ "unavailable_agents": "Unavailable locations", "no_agents": "No locations configured", "encryption_turned_off": "Encryption turned off", - "local_agent": "This system" + "local_agent": "This system", + "retention_all": "Keep all backups", + "retention_backups": "Keep {count} {count, plural,\n one {backup}\n other {backups}\n}", + "retention_days": "Keep {count} {count, plural,\n one {day}\n other {days}\n}" }, "data": { "ha_settings": "Home Assistant settings", From 3595fab5cbe319417210ca29d574030600f0bdd5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 6 May 2025 03:49:35 -0400 Subject: [PATCH 46/80] Show voice ID in TTS media browser (#25324) * Show voice ID in TTS media browser * Apply suggestions from code review Co-authored-by: Paul Bottein * Add copy button * Now copy correct clipboard icon * Improve styling * GAP --------- Co-authored-by: Paul Bottein --- .../media-player/ha-browse-media-tts.ts | 135 ++++++++++++------ src/translations/en.json | 4 +- 2 files changed, 94 insertions(+), 45 deletions(-) diff --git a/src/components/media-player/ha-browse-media-tts.ts b/src/components/media-player/ha-browse-media-tts.ts index e6fbdd62d7..1924af09a4 100644 --- a/src/components/media-player/ha-browse-media-tts.ts +++ b/src/components/media-player/ha-browse-media-tts.ts @@ -2,6 +2,7 @@ import "@material/mwc-button/mwc-button"; import type { PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { mdiContentCopy } from "@mdi/js"; import { storage } from "../../common/decorators/storage"; import { fireEvent } from "../../common/dom/fire_event"; import type { @@ -17,6 +18,8 @@ import "../ha-language-picker"; import "../ha-tts-voice-picker"; import "../ha-card"; import { fetchCloudStatus } from "../../data/cloud"; +import { copyToClipboard } from "../../common/util/copy-clipboard"; +import { showToast } from "../../util/toast"; export interface TtsMediaPickedEvent { item: MediaPlayerItem; @@ -51,50 +54,69 @@ class BrowseMediaTTS extends LitElement { private _message?: string; protected render() { - return html` -
- - - ${this._provider?.supported_languages?.length - ? html`
- - -
` - : nothing} -
-
- - ${this.hass.localize( - `ui.components.media-browser.tts.action_${this.action}` - )} - -
-
`; + return html` + +
+ + + ${this._provider?.supported_languages?.length + ? html`
+ + +
` + : nothing} +
+
+ + ${this.hass.localize( + `ui.components.media-browser.tts.action_${this.action}` + )} + +
+
+ ${this._voice + ? html` + + ` + : nothing} + `; } protected override willUpdate(changedProps: PropertyValues): void { @@ -197,6 +219,14 @@ class BrowseMediaTTS extends LitElement { fireEvent(this, "tts-picked", { item }); } + private async _copyVoiceId(ev) { + ev.preventDefault(); + await copyToClipboard(this._voice); + showToast(this, { + message: this.hass.localize("ui.common.copied_clipboard"), + }); + } + static override styles = [ buttonLinkStyle, css` @@ -218,6 +248,23 @@ class BrowseMediaTTS extends LitElement { button.link { color: var(--primary-color); } + .footer { + font-size: var(--ha-font-size-s); + color: var(--secondary-text-color); + margin: 16px 0; + text-align: center; + } + .footer code { + font-weight: var(--ha-font-weight-bold); + } + .footer { + --mdc-icon-size: 14px; + --mdc-icon-button-size: 24px; + display: flex; + justify-content: center; + align-items: center; + gap: 6px; + } `, ]; } diff --git a/src/translations/en.json b/src/translations/en.json index 10d0ce38df..69b6e55f76 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -920,7 +920,9 @@ "action_play": "Say", "action_pick": "Select", "set_as_default": "Set as default options", - "faild_to_store_defaults": "Failed to store defaults: {error}" + "faild_to_store_defaults": "Failed to store defaults: {error}", + "selected_voice_id": "Selected voice ID", + "copy_voice_id": "Copy voice ID" }, "pick": "Pick", "play": "Play", From 9c16ce334252b2fa325bd4d2fcf8e29068a58d3b Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Tue, 6 May 2025 13:52:00 +0200 Subject: [PATCH 47/80] Fix flow form padding end (#25328) * Fix flow form padding end * Use more end padding if docs are present --- .../config-flow/dialog-data-entry-flow.ts | 42 +++++++++++-------- src/dialogs/config-flow/step-flow-abort.ts | 5 ++- .../config-flow/step-flow-create-entry.ts | 5 ++- src/dialogs/config-flow/step-flow-external.ts | 10 ++++- src/dialogs/config-flow/step-flow-form.ts | 7 +++- src/dialogs/config-flow/step-flow-menu.ts | 7 +++- src/dialogs/config-flow/step-flow-progress.ts | 5 ++- src/dialogs/config-flow/styles.ts | 3 ++ 8 files changed, 61 insertions(+), 23 deletions(-) diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 94959185ec..726143bdb1 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -1,4 +1,3 @@ -import "@material/mwc-button"; import { mdiClose, mdiHelpCircle } from "@mdi/js"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { CSSResultGroup, PropertyValues } from "lit"; @@ -177,6 +176,17 @@ class DataEntryFlowDialog extends LitElement { return nothing; } + const showDocumentationLink = + ([ + "form", + "menu", + "external", + "progress", + "data_entry_flow_progressed", + ].includes(this._step?.type as any) && + this._params.manifest?.is_built_in) || + !!this._params.manifest?.documentation; + return html` @@ -199,26 +209,18 @@ class DataEntryFlowDialog extends LitElement { : this._step === undefined ? // When we are going to next step, we render 1 round of empty // to reset the element. - "" + nothing : html`
- ${([ - "form", - "menu", - "external", - "progress", - "data_entry_flow_progressed", - ].includes(this._step?.type as any) && - this._params.manifest?.is_built_in) || - this._params.manifest?.documentation + ${showDocumentationLink ? html` @@ -229,7 +231,7 @@ class DataEntryFlowDialog extends LitElement { ` - : ""} + : nothing} ` : this._step.type === "external" @@ -250,6 +253,7 @@ class DataEntryFlowDialog extends LitElement { .flowConfig=${this._params.flowConfig} .step=${this._step} .hass=${this.hass} + .increasePaddingEnd=${showDocumentationLink} > ` : this._step.type === "abort" @@ -261,6 +265,7 @@ class DataEntryFlowDialog extends LitElement { .handler=${this._step.handler} .domain=${this._params.domain ?? this._step.handler} + .increasePaddingEnd=${showDocumentationLink} > ` : this._step.type === "progress" @@ -270,6 +275,7 @@ class DataEntryFlowDialog extends LitElement { .step=${this._step} .hass=${this.hass} .progress=${this._progress} + .increasePaddingEnd=${showDocumentationLink} > ` : this._step.type === "menu" @@ -278,6 +284,7 @@ class DataEntryFlowDialog extends LitElement { .flowConfig=${this._params.flowConfig} .step=${this._step} .hass=${this.hass} + .increasePaddingEnd=${showDocumentationLink} > ` : html` @@ -286,7 +293,8 @@ class DataEntryFlowDialog extends LitElement { .step=${this._step} .hass=${this.hass} .navigateToResult=${this._params - .navigateToResult} + .navigateToResult ?? false} + .increasePaddingEnd=${showDocumentationLink} > `} `} diff --git a/src/dialogs/config-flow/step-flow-abort.ts b/src/dialogs/config-flow/step-flow-abort.ts index fa54d4ca57..9f0ad9abb0 100644 --- a/src/dialogs/config-flow/step-flow-abort.ts +++ b/src/dialogs/config-flow/step-flow-abort.ts @@ -22,6 +22,9 @@ class StepFlowAbort extends LitElement { @property({ attribute: false }) public handler!: string; + @property({ type: Boolean, attribute: "increase-padding-end" }) + public increasePaddingEnd = false; + protected firstUpdated(changed: PropertyValues) { super.firstUpdated(changed); if (this.step.reason === "missing_credentials") { @@ -34,7 +37,7 @@ class StepFlowAbort extends LitElement { return nothing; } return html` -

+

${this.params.flowConfig.renderAbortHeader ? this.params.flowConfig.renderAbortHeader(this.hass, this.step) : this.hass.localize(`component.${this.domain}.title`)} diff --git a/src/dialogs/config-flow/step-flow-create-entry.ts b/src/dialogs/config-flow/step-flow-create-entry.ts index 6f59bf593d..327b5b783a 100644 --- a/src/dialogs/config-flow/step-flow-create-entry.ts +++ b/src/dialogs/config-flow/step-flow-create-entry.ts @@ -36,6 +36,9 @@ class StepFlowCreateEntry extends LitElement { @property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry; + @property({ type: Boolean, attribute: "increase-padding-end" }) + public increasePaddingEnd = false; + public navigateToResult = false; @state() private _deviceUpdate: Record< @@ -113,7 +116,7 @@ class StepFlowCreateEntry extends LitElement { this.step.result?.entry_id ); return html` -

+

${devices.length ? localize("ui.panel.config.integrations.config_flow.assign_area", { number: devices.length, diff --git a/src/dialogs/config-flow/step-flow-external.ts b/src/dialogs/config-flow/step-flow-external.ts index 4e3b10512c..98d98c61ef 100644 --- a/src/dialogs/config-flow/step-flow-external.ts +++ b/src/dialogs/config-flow/step-flow-external.ts @@ -15,11 +15,16 @@ class StepFlowExternal extends LitElement { @property({ attribute: false }) public step!: DataEntryFlowStepExternal; + @property({ type: Boolean, attribute: "increase-padding-end" }) + public increasePaddingEnd = false; + protected render(): TemplateResult { const localize = this.hass.localize; return html` -

${this.flowConfig.renderExternalStepHeader(this.hass, this.step)}

+

+ ${this.flowConfig.renderExternalStepHeader(this.hass, this.step)} +

${this.flowConfig.renderExternalStepDescription(this.hass, this.step)}
@@ -51,6 +56,9 @@ class StepFlowExternal extends LitElement { .open-button a { text-decoration: none; } + h2.end-space { + padding-inline-end: 72px; + } `, ]; } diff --git a/src/dialogs/config-flow/step-flow-form.ts b/src/dialogs/config-flow/step-flow-form.ts index 0c6da5eda2..06ec17dc0d 100644 --- a/src/dialogs/config-flow/step-flow-form.ts +++ b/src/dialogs/config-flow/step-flow-form.ts @@ -27,6 +27,9 @@ class StepFlowForm extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; + @property({ type: Boolean, attribute: "increase-padding-end" }) + public increasePaddingEnd = false; + @state() private _loading = false; @state() private _stepData?: Record; @@ -43,7 +46,9 @@ class StepFlowForm extends LitElement { const stepData = this._stepDataProcessed; return html` -

${this.flowConfig.renderShowFormStepHeader(this.hass, this.step)}

+

+ ${this.flowConfig.renderShowFormStepHeader(this.hass, this.step)} +

${this.flowConfig.renderShowFormStepDescription(this.hass, this.step)} ${this._errorMsg diff --git a/src/dialogs/config-flow/step-flow-menu.ts b/src/dialogs/config-flow/step-flow-menu.ts index 7c1f1e35f6..b0fcd3ba64 100644 --- a/src/dialogs/config-flow/step-flow-menu.ts +++ b/src/dialogs/config-flow/step-flow-menu.ts @@ -17,6 +17,9 @@ class StepFlowMenu extends LitElement { @property({ attribute: false }) public step!: DataEntryFlowStepMenu; + @property({ type: Boolean, attribute: "increase-padding-end" }) + public increasePaddingEnd = false; + protected render(): TemplateResult { let options: string[]; let translations: Record; @@ -42,7 +45,9 @@ class StepFlowMenu extends LitElement { ); return html` -

${this.flowConfig.renderMenuHeader(this.hass, this.step)}

+

+ ${this.flowConfig.renderMenuHeader(this.hass, this.step)} +

${description ? html`
${description}
` : ""}
${options.map( diff --git a/src/dialogs/config-flow/step-flow-progress.ts b/src/dialogs/config-flow/step-flow-progress.ts index ef56fa271d..c71efcb98c 100644 --- a/src/dialogs/config-flow/step-flow-progress.ts +++ b/src/dialogs/config-flow/step-flow-progress.ts @@ -24,9 +24,12 @@ class StepFlowProgress extends LitElement { @property({ type: Number }) public progress?: number; + @property({ type: Boolean, attribute: "increase-padding-end" }) + public increasePaddingEnd = false; + protected render(): TemplateResult { return html` -

+

${this.flowConfig.renderShowFormProgressHeader(this.hass, this.step)}

diff --git a/src/dialogs/config-flow/styles.ts b/src/dialogs/config-flow/styles.ts index daaa9342af..0e2aca3e7b 100644 --- a/src/dialogs/config-flow/styles.ts +++ b/src/dialogs/config-flow/styles.ts @@ -22,6 +22,9 @@ export const configFlowContentStyles = css` text-transform: var(--mdc-typography-headline6-text-transform, inherit); box-sizing: border-box; } + h2.end-space { + padding-inline-end: 72px; + } .content, .preview { From 39119eeb2a80b905f0a9c35e6897f63de4e8412b Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 6 May 2025 10:57:47 +0200 Subject: [PATCH 48/80] Align side bar title with items (#25330) --- src/components/ha-sidebar.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index b59b15405e..19c4a8d633 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -852,8 +852,8 @@ class HaSidebar extends SubscribeMixin(LitElement) { color: var(--sidebar-icon-color); } .title { - margin-left: 19px; - margin-inline-start: 19px; + margin-left: 3px; + margin-inline-start: 3px; margin-inline-end: initial; width: 100%; display: none; From 7c288d17695185be0d62b48485991ba897ae843d Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Tue, 6 May 2025 13:52:28 +0200 Subject: [PATCH 49/80] Fix sidebar item text width (#25332) * Fix sidebar item text width to utilize full available space * Update src/components/ha-sidebar.ts Co-authored-by: Bram Kragten --------- Co-authored-by: Bram Kragten --- src/components/ha-sidebar.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 19c4a8d633..a286613cb0 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -940,7 +940,6 @@ class HaSidebar extends SubscribeMixin(LitElement) { ha-md-list-item .item-text { display: none; - max-width: calc(100% - 56px); font-weight: 500; font-size: 14px; } From bfac6e1516987e1870bb1380e2fae1d08e3ed5ed Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 6 May 2025 13:38:38 +0200 Subject: [PATCH 50/80] Add covers to overview view for area strategy (#25334) --- .../lovelace/strategies/areas/areas-overview-view-strategy.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/panels/lovelace/strategies/areas/areas-overview-view-strategy.ts b/src/panels/lovelace/strategies/areas/areas-overview-view-strategy.ts index 40a034140b..fde3545216 100644 --- a/src/panels/lovelace/strategies/areas/areas-overview-view-strategy.ts +++ b/src/panels/lovelace/strategies/areas/areas-overview-view-strategy.ts @@ -50,6 +50,7 @@ export class AreasOverviewViewStrategy extends ReactiveElement { const entities = [ ...groups.lights, + ...groups.covers, ...groups.climate, ...groups.media_players, ...groups.security, From 9629159ef140c148a1ff85021d990bfcd18e4a4b Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 6 May 2025 15:56:20 +0200 Subject: [PATCH 51/80] Use middle dot 00B7 as separator (#25336) --- src/components/data-table/ha-data-table.ts | 2 +- src/dialogs/more-info/controls/more-info-cover.ts | 2 +- src/dialogs/more-info/controls/more-info-valve.ts | 2 +- .../backup/components/config/ha-backup-config-agents.ts | 2 +- src/panels/config/info/ha-config-info.ts | 2 +- src/panels/config/logs/dialog-download-logs.ts | 2 +- src/panels/config/repairs/dialog-repairs-issue-subtitle.ts | 2 +- src/panels/config/repairs/ha-config-repairs.ts | 4 ++-- .../climate/ha-state-control-climate-temperature.ts | 2 +- src/state-display/state-display.ts | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index 39447ca320..66d7a3ba60 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -603,7 +603,7 @@ export class HaDataTable extends LitElement { .map( ([key2, column2], i) => html`${i !== 0 - ? " ⸱ " + ? " · " : nothing}${column2.template ? column2.template(row) : row[key2]}` diff --git a/src/dialogs/more-info/controls/more-info-cover.ts b/src/dialogs/more-info/controls/more-info-cover.ts index 166d5367a6..b041b44b5e 100644 --- a/src/dialogs/more-info/controls/more-info-cover.ts +++ b/src/dialogs/more-info/controls/more-info-cover.ts @@ -57,7 +57,7 @@ class MoreInfoCover extends LitElement { ); if (positionStateDisplay) { - return `${stateDisplay} ⸱ ${positionStateDisplay}`; + return `${stateDisplay} · ${positionStateDisplay}`; } return stateDisplay; } diff --git a/src/dialogs/more-info/controls/more-info-valve.ts b/src/dialogs/more-info/controls/more-info-valve.ts index 66158025c8..84a0e43eaf 100644 --- a/src/dialogs/more-info/controls/more-info-valve.ts +++ b/src/dialogs/more-info/controls/more-info-valve.ts @@ -57,7 +57,7 @@ class MoreInfoValve extends LitElement { ); if (positionStateDisplay) { - return `${stateDisplay} ⸱ ${positionStateDisplay}`; + return `${stateDisplay} · ${positionStateDisplay}`; } return stateDisplay; } diff --git a/src/panels/config/backup/components/config/ha-backup-config-agents.ts b/src/panels/config/backup/components/config/ha-backup-config-agents.ts index a9817dba96..f808d3fe99 100644 --- a/src/panels/config/backup/components/config/ha-backup-config-agents.ts +++ b/src/panels/config/backup/components/config/ha-backup-config-agents.ts @@ -102,7 +102,7 @@ class HaBackupConfigAgents extends LitElement { ); } } - return join(texts, html``); + return join(texts, html` · `); } private _availableAgents = memoizeOne( diff --git a/src/panels/config/info/ha-config-info.ts b/src/panels/config/info/ha-config-info.ts index 26acffcabd..d58f4a858e 100644 --- a/src/panels/config/info/ha-config-info.ts +++ b/src/panels/config/info/ha-config-info.ts @@ -156,7 +156,7 @@ class HaConfigInfo extends LitElement { )} - ${JS_VERSION}${JS_TYPE !== "modern" ? ` ⸱ ${JS_TYPE}` : ""} + ${JS_VERSION}${JS_TYPE !== "modern" ? ` · ${JS_TYPE}` : ""} diff --git a/src/panels/config/logs/dialog-download-logs.ts b/src/panels/config/logs/dialog-download-logs.ts index 801ef1db22..528235c4e5 100644 --- a/src/panels/config/logs/dialog-download-logs.ts +++ b/src/panels/config/logs/dialog-download-logs.ts @@ -70,7 +70,7 @@ class DownloadLogsDialog extends LitElement { ${this._dialogParams.header}${this._dialogParams.boot === 0 ? "" - : ` ⸱ ${this._dialogParams.boot === -1 ? this.hass.localize("ui.panel.config.logs.previous") : this.hass.localize("ui.panel.config.logs.startups_ago", { boot: this._dialogParams.boot * -1 })}`} + : ` · ${this._dialogParams.boot === -1 ? this.hass.localize("ui.panel.config.logs.previous") : this.hass.localize("ui.panel.config.logs.startups_ago", { boot: this._dialogParams.boot * -1 })}`}
diff --git a/src/panels/config/repairs/dialog-repairs-issue-subtitle.ts b/src/panels/config/repairs/dialog-repairs-issue-subtitle.ts index 6ea15ed6e4..d0c39bc951 100644 --- a/src/panels/config/repairs/dialog-repairs-issue-subtitle.ts +++ b/src/panels/config/repairs/dialog-repairs-issue-subtitle.ts @@ -20,7 +20,7 @@ class DialogRepairsIssueSubtitle extends LitElement { protected render() { const domainName = domainToName(this.hass.localize, this.issue.domain); const reportedBy = domainName - ? ` ⸱ ${this.hass.localize("ui.panel.config.repairs.reported_by", { + ? ` · ${this.hass.localize("ui.panel.config.repairs.reported_by", { integration: domainName, })}` : ""; diff --git a/src/panels/config/repairs/ha-config-repairs.ts b/src/panels/config/repairs/ha-config-repairs.ts index 77345f3e86..f4729b0287 100644 --- a/src/panels/config/repairs/ha-config-repairs.ts +++ b/src/panels/config/repairs/ha-config-repairs.ts @@ -100,13 +100,13 @@ class HaConfigRepairs extends LitElement { ${(issue.severity === "critical" || issue.severity === "error") && issue.created - ? " ⸱ " + ? " · " : ""} ${createdBy ? html`${createdBy}` : nothing} ${issue.ignored - ? ` ⸱ ${this.hass.localize( + ? ` · ${this.hass.localize( "ui.panel.config.repairs.dialog.ignored_in_version_short", { version: issue.dismissed_version } )}` diff --git a/src/state-control/climate/ha-state-control-climate-temperature.ts b/src/state-control/climate/ha-state-control-climate-temperature.ts index 6b2000222b..a3317dfde3 100644 --- a/src/state-control/climate/ha-state-control-climate-temperature.ts +++ b/src/state-control/climate/ha-state-control-climate-temperature.ts @@ -366,7 +366,7 @@ export class HaStateControlClimateTemperature extends LitElement { > ${this._renderTarget(this._targetTemperature.low!, "normal", true)} - + ·