From a6a340b5db7fe4f05215cc5de58a05d011b10ada Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 29 Oct 2025 17:05:34 +0100 Subject: [PATCH 01/66] Only display add button if at least one entity is selected in entities picker (#27699) --- src/components/entity/ha-entities-picker.ts | 2 +- src/components/ha-form/ha-form.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/entity/ha-entities-picker.ts b/src/components/entity/ha-entities-picker.ts index 46bbbf3742..6aaacab031 100644 --- a/src/components/entity/ha-entities-picker.ts +++ b/src/components/entity/ha-entities-picker.ts @@ -147,7 +147,7 @@ class HaEntitiesPicker extends LitElement { .createDomains=${this.createDomains} .required=${this.required && !currentEntities.length} @value-changed=${this._addEntity} - add-button + .addButton=${currentEntities.length > 0} > `; diff --git a/src/components/ha-form/ha-form.ts b/src/components/ha-form/ha-form.ts index 7b0d8b3906..71e5f03b53 100644 --- a/src/components/ha-form/ha-form.ts +++ b/src/components/ha-form/ha-form.ts @@ -148,7 +148,7 @@ export class HaForm extends LitElement implements HaFormElement { .value=${getValue(this.data, item)} .label=${this._computeLabel(item, this.data)} .disabled=${item.disabled || this.disabled || false} - .placeholder=${item.required ? "" : item.default} + .placeholder=${item.required ? undefined : item.default} .helper=${this._computeHelper(item)} .localizeValue=${this.localizeValue} .required=${item.required || false} From a5d27c8bb8151689c456b6d205f2eb4add538952 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 29 Oct 2025 17:02:02 +0100 Subject: [PATCH 02/66] Only clear from and to trigger in state trigger (#27700) --- .../trigger/types/ha-automation-trigger-state.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts index 1c9ed326ca..5fc0896d4f 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts @@ -245,18 +245,20 @@ export class HaStateTrigger extends LitElement implements TriggerElement { newTrigger.to, newTrigger.attribute ); + if (Array.isArray(newTrigger.to) && newTrigger.to.length === 0) { + delete newTrigger.to; + } newTrigger.from = this._applyAnyStateExclusive( newTrigger.from, newTrigger.attribute ); + if (Array.isArray(newTrigger.from) && newTrigger.from.length === 0) { + delete newTrigger.from; + } Object.keys(newTrigger).forEach((key) => { const val = newTrigger[key]; - if ( - val === undefined || - val === "" || - (Array.isArray(val) && val.length === 0) - ) { + if (val === undefined || val === "") { delete newTrigger[key]; } }); From cbab5c3f7b50a71300e01bd577c374dc52c504d4 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 30 Oct 2025 03:26:25 +0100 Subject: [PATCH 03/66] Restore trigger id in overflow menu for trigger (#27702) --- .../sidebar/ha-automation-sidebar-trigger.ts | 27 +++++++++++++++++++ .../trigger/ha-automation-trigger-editor.ts | 15 +++++------ src/translations/en.json | 1 - 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/panels/config/automation/sidebar/ha-automation-sidebar-trigger.ts b/src/panels/config/automation/sidebar/ha-automation-sidebar-trigger.ts index e8747650ec..d79b0fd8f3 100644 --- a/src/panels/config/automation/sidebar/ha-automation-sidebar-trigger.ts +++ b/src/panels/config/automation/sidebar/ha-automation-sidebar-trigger.ts @@ -3,6 +3,7 @@ import { mdiContentCopy, mdiContentCut, mdiDelete, + mdiIdentifier, mdiPlayCircleOutline, mdiPlaylistEdit, mdiPlusCircleMultipleOutline, @@ -40,6 +41,8 @@ export default class HaAutomationSidebarTrigger extends LitElement { @property({ type: Number, attribute: "sidebar-key" }) public sidebarKey?: number; + @state() private _requestShowId = false; + @state() private _warnings?: string[]; @query(".sidebar-editor") @@ -47,6 +50,7 @@ export default class HaAutomationSidebarTrigger extends LitElement { protected willUpdate(changedProperties) { if (changedProperties.has("config")) { + this._requestShowId = false; this._warnings = undefined; if (this.config) { this.yamlMode = this.config.yamlMode; @@ -101,6 +105,24 @@ export default class HaAutomationSidebarTrigger extends LitElement { + ${!this.yamlMode && + !("id" in this.config.config) && + !this._requestShowId + ? html` + +
+ ${this.hass.localize( + "ui.panel.config.automation.editor.triggers.edit_id" + )} + +
+
` + : nothing} + { + this._requestShowId = true; + }; + static styles = [sidebarEditorStyles, overflowStyles]; } diff --git a/src/panels/config/automation/trigger/ha-automation-trigger-editor.ts b/src/panels/config/automation/trigger/ha-automation-trigger-editor.ts index 77390b377b..d9ba61fad4 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger-editor.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger-editor.ts @@ -29,6 +29,8 @@ export default class HaAutomationTriggerEditor extends LitElement { @property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false; + @property({ type: Boolean, attribute: "show-id" }) public showId = false; + @query("ha-yaml-editor") public yamlEditor?: HaYamlEditor; protected render() { @@ -36,6 +38,8 @@ export default class HaAutomationTriggerEditor extends LitElement { const yamlMode = this.yamlMode || !this.uiSupported; + const showId = "id" in this.trigger || this.showId; + return html`
` : html` - ${!isTriggerList(this.trigger) + ${showId && !isTriggerList(this.trigger) ? html` ` : nothing} diff --git a/src/translations/en.json b/src/translations/en.json index c551a1a59e..1bdcfbc370 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3945,7 +3945,6 @@ "add": "Add trigger", "empty_search": "No triggers found for {term}", "id": "Trigger ID", - "id_helper": "Helps identify each run based on which trigger fired.", "optional": "Optional", "edit_id": "Edit ID", "duplicate": "[%key:ui::common::duplicate%]", From eecd8077b60cfbed8e05105bb93940e6aad7dbb1 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Thu, 30 Oct 2025 14:47:27 +0000 Subject: [PATCH 04/66] Calendar card height: account for title and stop overflow (#27707) --- src/panels/lovelace/cards/hui-calendar-card.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/panels/lovelace/cards/hui-calendar-card.ts b/src/panels/lovelace/cards/hui-calendar-card.ts index adea17966b..798ce24afc 100644 --- a/src/panels/lovelace/cards/hui-calendar-card.ts +++ b/src/panels/lovelace/cards/hui-calendar-card.ts @@ -137,6 +137,7 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard { class=${classMap({ "is-grid": this.layout === "grid", "is-panel": this.layout === "panel", + "has-title": !!this._config.title, })} .narrow=${this._narrow} .events=${this._events} @@ -229,6 +230,7 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard { padding: 0 8px 8px; box-sizing: border-box; height: 100%; + overflow: hidden; } .header { @@ -239,15 +241,25 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard { padding-left: 8px; padding-inline-start: 8px; direction: var(--direction); + white-space: nowrap; + text-overflow: ellipsis; } ha-full-calendar { --calendar-height: 400px; + height: var(--calendar-height); } ha-full-calendar.is-grid, ha-full-calendar.is-panel { - height: calc(100% - 16px); + --calendar-height: calc(100% - 16px); + } + + ha-full-calendar.is-grid.has-title, + ha-full-calendar.is-panel.has-title { + --calendar-height: calc( + 100% - var(--ha-card-header-font-size, var(--ha-font-size-2xl)) - 22px + ); } `; } From 7560988b763ed68f4623104c2b1da52531949a52 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Thu, 30 Oct 2025 08:43:04 +0000 Subject: [PATCH 05/66] Trend feature: make sure content is centered when loading (#27708) * Make sure content is centered when loading * Restore from test --- .../card-features/hui-trend-graph-card-feature.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/panels/lovelace/card-features/hui-trend-graph-card-feature.ts b/src/panels/lovelace/card-features/hui-trend-graph-card-feature.ts index dfe3596c4b..af74e9408f 100644 --- a/src/panels/lovelace/card-features/hui-trend-graph-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-trend-graph-card-feature.ts @@ -94,7 +94,7 @@ class HuiHistoryChartCardFeature } if (!this._coordinates) { return html` -
+
`; @@ -153,6 +153,14 @@ class HuiHistoryChartCardFeature align-items: flex-end; pointer-events: none !important; } + + .container.loading { + width: 100%; + display: flex; + justify-content: center; + align-items: center; + } + hui-graph-base { width: 100%; --accent-color: var(--feature-color); From e88c97d6252578e3ce329135ca0acae3207e6e8d Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 30 Oct 2025 16:58:52 +0100 Subject: [PATCH 06/66] Use entity naming in more cards (#27714) * Use entity naming in more cards * Migrate statistic card * Fix localize --- cast/src/launcher/layout/hc-cast.ts | 7 +-- .../device-detail/ha-device-entities-card.ts | 2 +- src/panels/lovelace/cards/hui-glance-card.ts | 8 ++- .../lovelace/cards/hui-history-graph-card.ts | 44 ++++++++++----- src/panels/lovelace/cards/hui-map-card.ts | 10 +--- .../lovelace/cards/hui-statistic-card.ts | 7 ++- .../cards/hui-statistics-graph-card.ts | 54 ++++++++++++------- src/panels/lovelace/cards/types.ts | 16 ++++-- .../common/generate-lovelace-config.ts | 44 ++++++++------- .../common/process-config-entities.ts | 6 ++- .../lovelace/components/hui-entity-editor.ts | 11 ++-- .../components/hui-generic-entity-row.ts | 12 +++-- .../card-editor/hui-dialog-create-card.ts | 6 +-- .../hui-generic-entity-row-editor.ts | 26 +++++---- .../config-elements/hui-glance-card-editor.ts | 16 ++++-- .../hui-history-graph-card-editor.ts | 8 ++- .../config-elements/hui-map-card-editor.ts | 16 +++--- .../hui-statistic-card-editor.ts | 9 +++- .../editor/structs/entities-struct.ts | 3 +- src/panels/lovelace/editor/types.ts | 5 +- .../unused-entities/hui-unused-entities.ts | 6 +-- .../entity-rows/hui-datetime-entity-row.ts | 16 ++++-- .../hui-input-datetime-entity-row.ts | 8 ++- .../hui-input-select-entity-row.ts | 10 +++- .../entity-rows/hui-input-text-entity-row.ts | 10 +++- .../entity-rows/hui-select-entity-row.ts | 10 +++- .../entity-rows/hui-text-entity-row.ts | 10 +++- .../entity-rows/hui-weather-entity-row.ts | 10 +++- src/panels/lovelace/entity-rows/types.ts | 3 +- .../lovelace/special-rows/hui-button-row.ts | 13 +++-- .../original-states-view-strategy.ts | 5 +- 31 files changed, 259 insertions(+), 152 deletions(-) diff --git a/cast/src/launcher/layout/hc-cast.ts b/cast/src/launcher/layout/hc-cast.ts index 9a15f58f44..2af8599fa2 100644 --- a/cast/src/launcher/layout/hc-cast.ts +++ b/cast/src/launcher/layout/hc-cast.ts @@ -16,9 +16,9 @@ import { } from "../../../../src/common/auth/token_storage"; import { atLeastVersion } from "../../../../src/common/config/version"; import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute"; +import "../../../../src/components/ha-button"; import "../../../../src/components/ha-icon"; import "../../../../src/components/ha-list"; -import "../../../../src/components/ha-button"; import "../../../../src/components/ha-list-item"; import "../../../../src/components/ha-svg-icon"; import { @@ -28,7 +28,6 @@ import { import { isStrategyDashboard } from "../../../../src/data/lovelace/config/types"; import type { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view"; import "../../../../src/layouts/hass-loading-screen"; -import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config"; import "./hc-layout"; @customElement("hc-cast") @@ -96,7 +95,9 @@ class HcCast extends LitElement { ${( this.lovelaceViews ?? [ - generateDefaultViewConfig({}, {}, {}, {}, () => ""), + { + title: "Home", + }, ] ).map( (view, idx) => html` diff --git a/src/panels/config/devices/device-detail/ha-device-entities-card.ts b/src/panels/config/devices/device-detail/ha-device-entities-card.ts index 7c8d626e80..a20f29828d 100644 --- a/src/panels/config/devices/device-detail/ha-device-entities-card.ts +++ b/src/panels/config/devices/device-detail/ha-device-entities-card.ts @@ -228,7 +228,7 @@ export class HaDeviceEntitiesCard extends LitElement { addEntitiesToLovelaceView( this, this.hass, - computeCards(this.hass.states, entities, { + computeCards(this.hass, entities, { title: this.deviceName, }), computeSection(entities, { diff --git a/src/panels/lovelace/cards/hui-glance-card.ts b/src/panels/lovelace/cards/hui-glance-card.ts index 197c3c3e4b..f8aa4ece03 100644 --- a/src/panels/lovelace/cards/hui-glance-card.ts +++ b/src/panels/lovelace/cards/hui-glance-card.ts @@ -5,7 +5,6 @@ import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { computeDomain } from "../../../common/entity/compute_domain"; -import { computeStateName } from "../../../common/entity/compute_state_name"; import "../../../components/entity/state-badge"; import "../../../components/ha-card"; import "../../../components/ha-icon"; @@ -19,6 +18,7 @@ import type { import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor"; import type { HomeAssistant } from "../../../types"; import { actionHandler } from "../common/directives/action-handler-directive"; +import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; import { findEntities } from "../common/find-entities"; import { handleAction } from "../common/handle-action"; import { hasAction, hasAnyAction } from "../common/has-action"; @@ -252,7 +252,11 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
`; } - const name = entityConf.name ?? computeStateName(stateObj); + const name = computeLovelaceEntityName( + this.hass!, + stateObj, + entityConf.name + ); return html`
{ - this._entityIds.push(entity.entity); - if (entity.name) { - this._names[entity.entity] = entity.name; - } - }); + this._entityIds = this._entities.map((entity) => entity.entity); this._hoursToShow = config.hours_to_show || DEFAULT_HOURS_TO_SHOW; this._config = config; + this._computeNames(); + } + + private _computeNames() { + if (!this.hass || !this._config) { + return; + } + this._names = {}; + this._entities.forEach((entity) => { + const stateObj = this.hass!.states[entity.entity]; + this._names[entity.entity] = stateObj + ? computeLovelaceEntityName(this.hass!, stateObj, entity.name) + : entity.entity; + }); + } + + public willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); + if (changedProps.has("hass")) { + this._computeNames(); + } } public connectedCallback() { diff --git a/src/panels/lovelace/cards/hui-map-card.ts b/src/panels/lovelace/cards/hui-map-card.ts index b6eaf4cfde..670b3977bc 100644 --- a/src/panels/lovelace/cards/hui-map-card.ts +++ b/src/panels/lovelace/cards/hui-map-card.ts @@ -35,20 +35,12 @@ import { hasConfigOrEntitiesChanged, } from "../common/has-changed"; import { processConfigEntities } from "../common/process-config-entities"; -import type { EntityConfig } from "../entity-rows/types"; import type { LovelaceCard, LovelaceGridOptions } from "../types"; -import type { MapCardConfig } from "./types"; +import type { MapCardConfig, MapEntityConfig } from "./types"; export const DEFAULT_HOURS_TO_SHOW = 0; export const DEFAULT_ZOOM = 14; -interface MapEntityConfig extends EntityConfig { - label_mode?: "state" | "attribute" | "name"; - attribute?: string; - unit?: string; - focus?: boolean; -} - interface GeoEntity { entity_id: string; label_mode?: "state" | "attribute" | "name" | "icon"; diff --git a/src/panels/lovelace/cards/hui-statistic-card.ts b/src/panels/lovelace/cards/hui-statistic-card.ts index d8fca526f7..31efdeab4f 100644 --- a/src/panels/lovelace/cards/hui-statistic-card.ts +++ b/src/panels/lovelace/cards/hui-statistic-card.ts @@ -20,14 +20,15 @@ import { } from "../../../data/recorder"; import type { HomeAssistant } from "../../../types"; import { computeCardSize } from "../common/compute-card-size"; +import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; import { findEntities } from "../common/find-entities"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import { createHeaderFooterElement } from "../create-element/create-header-footer-element"; import type { LovelaceCard, LovelaceCardEditor, - LovelaceHeaderFooter, LovelaceGridOptions, + LovelaceHeaderFooter, } from "../types"; import type { HuiErrorCard } from "./hui-error-card"; import type { EntityCardConfig, StatisticCardConfig } from "./types"; @@ -180,7 +181,9 @@ export class HuiStatisticCard extends LitElement implements LovelaceCard { const stateObj = this.hass.states[this._config.entity]; const name = - this._config.name || + (this._config.name + ? computeLovelaceEntityName(this.hass, stateObj, this._config.name) + : "") || getStatisticLabel(this.hass, this._config.entity, this._metadata); return html` diff --git a/src/panels/lovelace/cards/hui-statistics-graph-card.ts b/src/panels/lovelace/cards/hui-statistics-graph-card.ts index 7d493f1575..6c6c1b1fab 100644 --- a/src/panels/lovelace/cards/hui-statistics-graph-card.ts +++ b/src/panels/lovelace/cards/hui-statistics-graph-card.ts @@ -1,15 +1,11 @@ +import { differenceInDays, subHours } from "date-fns"; import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; -import { subHours, differenceInDays } from "date-fns"; 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 "../../../components/ha-card"; import { getEnergyDataCollection } from "../../../data/energy"; -import { - getSuggestedMax, - getSuggestedPeriod, -} from "./energy/common/energy-chart-options"; import type { Statistics, StatisticsMetaData, @@ -21,10 +17,16 @@ import { getStatisticMetadata, } from "../../../data/recorder"; import type { HomeAssistant } from "../../../types"; +import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; import { findEntities } from "../common/find-entities"; import { hasConfigOrEntitiesChanged } from "../common/has-changed"; import { processConfigEntities } from "../common/process-config-entities"; +import type { EntityConfig } from "../entity-rows/types"; import type { LovelaceCard, LovelaceGridOptions } from "../types"; +import { + getSuggestedMax, + getSuggestedPeriod, +} from "./energy/common/energy-chart-options"; import type { StatisticsGraphCardConfig } from "./types"; export const DEFAULT_DAYS_TO_SHOW = 30; @@ -67,7 +69,9 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { @state() private _unit?: string; - private _entities: string[] = []; + private _entities: EntityConfig[] = []; + + private _entityIds: string[] = []; private _names: Record = {}; @@ -148,17 +152,10 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { throw new Error("You must include at least one entity"); } - const configEntities = config.entities + this._entities = config.entities ? processConfigEntities(config.entities, false) : []; - - this._entities = []; - configEntities.forEach((entity) => { - this._entities.push(entity.entity); - if (entity.name) { - this._names[entity.entity] = entity.name; - } - }); + this._entityIds = this._entities.map((ent) => ent.entity); if (typeof config.stat_types === "string") { this._statTypes = [config.stat_types]; @@ -168,6 +165,20 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { this._statTypes = config.stat_types; } this._config = config; + this._computeNames(); + } + + private _computeNames() { + if (!this.hass || !this._config) { + return; + } + this._names = {}; + this._entities.forEach((config) => { + const stateObj = this.hass!.states[config.entity]; + this._names[config.entity] = stateObj + ? computeLovelaceEntityName(this.hass!, stateObj, config.name) + : config.entity; + }); } protected shouldUpdate(changedProps: PropertyValues): boolean { @@ -209,6 +220,10 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { } } + if (changedProps.has("hass")) { + this._computeNames(); + } + if ( changedProps.has("_config") && oldConfig?.entities !== this._config.entities @@ -232,7 +247,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { clearInterval(this._interval); this._interval = 0; // block concurrent calls if (fetchMetadata) { - await this._getStatisticsMetaData(this._entities); + await this._getStatisticsMetaData(this._entityIds); } await this._getStatistics(); // statistics are created every hour @@ -344,7 +359,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { } } if (!unitClass && this._metadata) { - const metadata = this._metadata[this._entities[0]]; + const metadata = this._metadata[this._entityIds[0]]; unitClass = metadata?.unit_class; this._unit = unitClass ? getDisplayUnit(this.hass!, metadata.statistic_id, metadata) || @@ -356,14 +371,15 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { this.hass!, startDate, endDate, - this._entities, + this._entityIds, this._period, unitconfig, this._statTypes ); this._statistics = {}; - this._entities.forEach((id) => { + this._entities.forEach((entity) => { + const id = entity.entity; if (id in statistics) { this._statistics![id] = statistics[id]; } diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index b90bffb75a..ecaa18f758 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -5,6 +5,7 @@ import type { EnergySourceByType } from "../../../data/energy"; import type { ActionConfig } from "../../../data/lovelace/config/action"; import type { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import type { Statistic, StatisticType } from "../../../data/recorder"; +import type { MediaSelectorValue } from "../../../data/selector"; import type { TimeFormat } from "../../../data/translation"; import type { ForecastType } from "../../../data/weather"; import type { @@ -29,7 +30,6 @@ import type { import type { LovelaceHeaderFooterConfig } from "../header-footer/types"; import type { LovelaceHeadingBadgeConfig } from "../heading-badges/types"; import type { HomeSummary } from "../strategies/home/helpers/home-summaries"; -import type { MediaSelectorValue } from "../../../data/selector"; export type AlarmPanelCardConfigState = | "arm_away" @@ -347,7 +347,15 @@ export interface LogbookCardConfig extends LovelaceCardConfig { theme?: string; } -interface GeoLocationSourceConfig { +export interface MapEntityConfig extends EntityConfig { + label_mode?: "state" | "attribute" | "name"; + attribute?: string; + unit?: string; + focus?: boolean; + name?: string; +} + +export interface GeoLocationSourceConfig { source: string; label_mode?: "name" | "state" | "attribute" | "icon"; attribute?: string; @@ -362,7 +370,7 @@ export interface MapCardConfig extends LovelaceCardConfig { auto_fit?: boolean; fit_zones?: boolean; default_zoom?: number; - entities?: (EntityConfig | string)[]; + entities?: (MapEntityConfig | string)[]; hours_to_show?: number; geo_location_sources?: (GeoLocationSourceConfig | string)[]; dark_mode?: boolean; @@ -434,7 +442,7 @@ export interface StatisticsGraphCardConfig extends EnergyCardBaseConfig { } export interface StatisticCardConfig extends LovelaceCardConfig { - name?: string; + name?: string | EntityNameItem | EntityNameItem[]; entities: (EntityConfig | string)[]; period: | { diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts index 16d348b731..0beee6292d 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -1,5 +1,5 @@ import type { HassEntities, HassEntity } from "home-assistant-js-websocket"; -import { SENSOR_ENTITIES, ASSIST_ENTITIES } from "../../../common/const"; +import { ASSIST_ENTITIES, SENSOR_ENTITIES } from "../../../common/const"; import { computeDomain } from "../../../common/entity/compute_domain"; import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { computeStateName } from "../../../common/entity/compute_state_name"; @@ -14,12 +14,14 @@ import type { GridSourceTypeEnergyPreference, } from "../../../data/energy"; import { domainToName } from "../../../data/integration"; +import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; import type { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section"; import type { LovelaceViewConfig } from "../../../data/lovelace/config/view"; import { computeUserInitials } from "../../../data/user"; import type { HomeAssistant } from "../../../types"; import { HELPER_DOMAINS } from "../../config/helpers/const"; +import type { EntityBadgeConfig } from "../badges/types"; import type { AlarmPanelCardConfig, EntitiesCardConfig, @@ -31,8 +33,7 @@ import type { } from "../cards/types"; import type { EntityConfig } from "../entity-rows/types"; import type { ButtonsHeaderFooterConfig } from "../header-footer/types"; -import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; -import type { EntityBadgeConfig } from "../badges/types"; +import { computeLovelaceEntityName } from "./entity/compute-lovelace-entity-name"; const HIDE_DOMAIN = new Set([ "ai_task", @@ -125,13 +126,13 @@ export const computeSection = ( }); export const computeCards = ( - states: HassEntities, + hass: HomeAssistant, entityIds: string[], entityCardOptions: Partial, renderFooterEntities = true ): LovelaceCardConfig[] => { const cards: LovelaceCardConfig[] = []; - + const states = hass.states; // For entity card const entitiesConf: (string | EntityConfig)[] = []; @@ -270,19 +271,23 @@ export const computeCards = ( ? states[a] ? computeStateName(states[a]) : "" - : a.name || "", + : states[a.entity] + ? computeLovelaceEntityName(hass, states[a.entity], a.name) + : "", typeof b === "string" ? states[b] ? computeStateName(states[b]) : "" - : b.name || "" + : states[b.entity] + ? computeLovelaceEntityName(hass, states[b.entity], b.name) + : "" ); }); // If we ended up with footer entities but no normal entities, // render the footer entities as normal entities. if (entitiesConf.length === 0 && footerEntities.length > 0) { - return computeCards(states, entityIds, entityCardOptions, false); + return computeCards(hass, entityIds, entityCardOptions, false); } if (entitiesConf.length > 0 || footerEntities.length > 0) { @@ -360,14 +365,14 @@ const computeDefaultViewStates = ( }; export const generateViewConfig = ( - localize: LocalizeFunc, + hass: HomeAssistant, path: string, title: string | undefined, icon: string | undefined, entities: HassEntities ): LovelaceViewConfig => { const ungroupedEntitites: Record = {}; - + const { localize } = hass; // Organize ungrouped entities in ungrouped things for (const entityId of Object.keys(entities)) { const state = entities[entityId]; @@ -470,7 +475,7 @@ export const generateViewConfig = ( .forEach((domain) => { cards.push( ...computeCards( - entities, + hass, ungroupedEntitites[domain].sort((a, b) => stringCompare( computeStateName(entities[a]), @@ -498,16 +503,17 @@ export const generateViewConfig = ( }; export const generateDefaultViewConfig = ( - areaEntries: HomeAssistant["areas"], - deviceEntries: HomeAssistant["devices"], - entityEntries: HomeAssistant["entities"], - entities: HassEntities, + hass: HomeAssistant, localize: LocalizeFunc, energyPrefs?: EnergyPreferences, areasPrefs?: AreasDisplayValue, hideEntitiesWithoutAreas?: boolean, hideEnergy?: boolean ): LovelaceViewConfig => { + const entities = hass.states; + const areaEntries = hass.areas; + const deviceEntries = hass.devices; + const entityEntries = hass.entities; const states = computeDefaultViewStates(entities, entityEntries); const path = "default_view"; const title = "Home"; @@ -549,7 +555,7 @@ export const generateDefaultViewConfig = ( for (const groupEntity of splittedByGroups.groups) { groupCards.push( - ...computeCards(entities, groupEntity.attributes.entity_id, { + ...computeCards(hass, groupEntity.attributes.entity_id, { title: computeStateName(groupEntity), show_header_toggle: groupEntity.attributes.control !== "hidden", }) @@ -557,7 +563,7 @@ export const generateDefaultViewConfig = ( } const config = generateViewConfig( - localize, + hass, path, title, icon, @@ -575,7 +581,7 @@ export const generateDefaultViewConfig = ( const area = areaEntries[areaId]; areaCards.push( ...computeCards( - entities, + hass, areaEntities.map((entity) => entity.entity_id), { title: area.name, @@ -601,7 +607,7 @@ export const generateDefaultViewConfig = ( const device = deviceEntries[deviceId]; deviceCards.push( ...computeCards( - entities, + hass, deviceEntities.map((entity) => entity.entity_id), { title: diff --git a/src/panels/lovelace/common/process-config-entities.ts b/src/panels/lovelace/common/process-config-entities.ts index c51e28ebf8..f9ff1d85c8 100644 --- a/src/panels/lovelace/common/process-config-entities.ts +++ b/src/panels/lovelace/common/process-config-entities.ts @@ -2,8 +2,12 @@ import { isValidEntityId } from "../../../common/entity/valid_entity_id"; import type { EntityConfig, LovelaceRowConfig } from "../entity-rows/types"; +interface BaseEntityConfig { + type: string; + entity: string; +} export const processConfigEntities = < - T extends EntityConfig | LovelaceRowConfig, + T extends BaseEntityConfig | LovelaceRowConfig, >( entities: (T | string)[], checkEntityId = true diff --git a/src/panels/lovelace/components/hui-entity-editor.ts b/src/panels/lovelace/components/hui-entity-editor.ts index 78feddd713..36a0415d47 100644 --- a/src/panels/lovelace/components/hui-entity-editor.ts +++ b/src/panels/lovelace/components/hui-entity-editor.ts @@ -45,14 +45,13 @@ export class HuiEntityEditor extends LitElement { this.hass.devices ); - const name = this.hass.formatEntityName( - stateObj, - useDeviceName ? { type: "device" } : { type: "entity" } - ); - const isRTL = computeRTL(this.hass); - const primary = item.name || name || item.entity; + const primary = + this.hass.formatEntityName( + stateObj, + useDeviceName ? { type: "device" } : { type: "entity" } + ) || item.entity; const secondary = this.hass.formatEntityName( stateObj, diff --git a/src/panels/lovelace/components/hui-generic-entity-row.ts b/src/panels/lovelace/components/hui-generic-entity-row.ts index ec9c22a1cc..ace6a1549c 100644 --- a/src/panels/lovelace/components/hui-generic-entity-row.ts +++ b/src/panels/lovelace/components/hui-generic-entity-row.ts @@ -4,19 +4,19 @@ import { customElement, property } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; import { DOMAINS_INPUT_ROW } from "../../../common/const"; +import { stopPropagation } from "../../../common/dom/stop_propagation"; import { toggleAttribute } from "../../../common/dom/toggle_attribute"; import { computeDomain } from "../../../common/entity/compute_domain"; -import { computeStateName } from "../../../common/entity/compute_state_name"; import "../../../components/entity/state-badge"; import "../../../components/ha-relative-time"; import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; import type { HomeAssistant } from "../../../types"; import type { EntitiesCardEntityConfig } from "../cards/types"; import { actionHandler } from "../common/directives/action-handler-directive"; +import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; import { handleAction } from "../common/handle-action"; import { hasAction, hasAnyAction } from "../common/has-action"; import { createEntityNotFoundWarning } from "./hui-warning"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; @customElement("hui-generic-entity-row") export class HuiGenericEntityRow extends LitElement { @@ -59,7 +59,11 @@ export class HuiGenericEntityRow extends LitElement { const pointer = hasAnyAction(this.config); const hasSecondary = this.secondaryText || this.config.secondary_info; - const name = this.config.name ?? computeStateName(stateObj); + const name = computeLovelaceEntityName( + this.hass, + stateObj, + this.config.name + ); return html`
- ${this.config.name || computeStateName(stateObj)} + ${name} ${hasSecondary ? html`
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 c22c6f2d9a..e5f14f6ade 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 @@ -296,11 +296,7 @@ export class HuiCreateDialogCard } private _suggestCards(): void { - const cardConfig = computeCards( - this.hass.states, - this._selectedEntities, - {} - ); + const cardConfig = computeCards(this.hass, this._selectedEntities, {}); let sectionOptions: Partial = {}; diff --git a/src/panels/lovelace/editor/config-elements/hui-generic-entity-row-editor.ts b/src/panels/lovelace/editor/config-elements/hui-generic-entity-row-editor.ts index ec1f009526..79b9d95999 100644 --- a/src/panels/lovelace/editor/config-elements/hui-generic-entity-row-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-generic-entity-row-editor.ts @@ -46,20 +46,18 @@ export class HuiGenericEntityRowEditor return [ { name: "entity", required: true, selector: { entity: {} } }, { - type: "grid", - name: "", - schema: [ - { name: "name", selector: { text: {} } }, - { - name: "icon", - selector: { - icon: {}, - }, - context: { - icon_entity: "entity", - }, - }, - ], + name: "name", + selector: { entity_name: {} }, + context: { entity: "entity" }, + }, + { + name: "icon", + selector: { + icon: {}, + }, + context: { + icon_entity: "entity", + }, }, { name: "secondary_info", diff --git a/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts index fedab67069..641ed7a96a 100644 --- a/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts @@ -11,20 +11,20 @@ import { string, union, } from "superstruct"; +import type { HASSDomEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-form/ha-form"; -import "../hui-sub-element-editor"; -import type { EditDetailElementEvent, SubElementEditorConfig } from "../types"; -import type { HASSDomEvent } from "../../../../common/dom/fire_event"; import type { SchemaUnion } from "../../../../components/ha-form/types"; import type { HomeAssistant } from "../../../../types"; import type { ConfigEntity, GlanceCardConfig } from "../../cards/types"; import "../../components/hui-entity-editor"; +import type { EntityConfig } from "../../entity-rows/types"; import type { LovelaceCardEditor } from "../../types"; +import "../hui-sub-element-editor"; import { processEditorEntities } from "../process-editor-entities"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { entitiesConfigStruct } from "../structs/entities-struct"; -import type { EntityConfig } from "../../entity-rows/types"; +import type { EditDetailElementEvent, SubElementEditorConfig } from "../types"; const cardConfigStruct = assign( baseLovelaceCardConfig, @@ -42,11 +42,17 @@ const cardConfigStruct = assign( const SUB_SCHEMA = [ { name: "entity", selector: { entity: {} }, required: true }, + { + name: "name", + selector: { entity_name: {} }, + context: { + entity: "entity", + }, + }, { type: "grid", name: "", schema: [ - { name: "name", selector: { text: {} } }, { name: "icon", selector: { diff --git a/src/panels/lovelace/editor/config-elements/hui-history-graph-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-history-graph-card-editor.ts index b7f4e73e29..38134d8c65 100644 --- a/src/panels/lovelace/editor/config-elements/hui-history-graph-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-history-graph-card-editor.ts @@ -45,7 +45,13 @@ const cardConfigStruct = assign( const SUB_SCHEMA = [ { name: "entity", selector: { entity: {} }, required: true }, - { name: "name", selector: { text: {} } }, + { + name: "name", + selector: { entity_name: {} }, + context: { + entity: "entity", + }, + }, ] as const; @customElement("hui-history-graph-card-editor") diff --git a/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts index 365daf51ed..724018419d 100644 --- a/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts @@ -2,6 +2,7 @@ import { mdiPalette } from "@mdi/js"; import type { CSSResultGroup } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { array, assert, @@ -13,19 +14,19 @@ import { string, union, } from "superstruct"; -import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../common/dom/fire_event"; -import { hasLocation } from "../../../../common/entity/has_location"; import { computeDomain } from "../../../../common/entity/compute_domain"; +import { hasLocation } from "../../../../common/entity/has_location"; +import type { LocalizeFunc } from "../../../../common/translations/localize"; import "../../../../components/ha-form/ha-form"; import type { SchemaUnion } from "../../../../components/ha-form/types"; -import type { SelectSelector } from "../../../../data/selector"; import "../../../../components/ha-formfield"; -import "../../../../components/ha-switch"; import "../../../../components/ha-selector/ha-selector-select"; +import "../../../../components/ha-switch"; +import type { SelectSelector } from "../../../../data/selector"; import type { HomeAssistant, ValueChangedEvent } from "../../../../types"; import { DEFAULT_HOURS_TO_SHOW, DEFAULT_ZOOM } from "../../cards/hui-map-card"; -import type { MapCardConfig } from "../../cards/types"; +import type { MapCardConfig, MapEntityConfig } from "../../cards/types"; import "../../components/hui-entity-editor"; import type { EntityConfig } from "../../entity-rows/types"; import type { LovelaceCardEditor } from "../../types"; @@ -33,7 +34,6 @@ import { processEditorEntities } from "../process-editor-entities"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import type { EntitiesEditorEvent } from "../types"; import { configElementStyle } from "./config-elements-style"; -import type { LocalizeFunc } from "../../../../common/translations/localize"; export const mapEntitiesConfigStruct = union([ object({ @@ -223,7 +223,9 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor { }) ); - private _entitiesValueChanged(ev: EntitiesEditorEvent): void { + private _entitiesValueChanged( + ev: EntitiesEditorEvent + ): void { if (ev.detail && ev.detail.entities) { this._config = { ...this._config!, entities: ev.detail.entities }; diff --git a/src/panels/lovelace/editor/config-elements/hui-statistic-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-statistic-card-editor.ts index 53368424b7..2b50c1bcbe 100644 --- a/src/panels/lovelace/editor/config-elements/hui-statistic-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-statistic-card-editor.ts @@ -21,12 +21,13 @@ import type { StatisticCardConfig } from "../../cards/types"; import { headerFooterConfigStructs } from "../../header-footer/structs"; import type { LovelaceCardEditor } from "../../types"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; +import { entityNameStruct } from "../structs/entity-name-struct"; const cardConfigStruct = assign( baseLovelaceCardConfig, object({ entity: optional(string()), - name: optional(string()), + name: optional(entityNameStruct), icon: optional(string()), unit: optional(string()), stat_type: optional(string()), @@ -144,11 +145,15 @@ export class HuiStatisticCardEditor } : { object: {} }, }, + { + name: "name", + selector: { entity_name: {} }, + context: { entity: "entity" }, + }, { type: "grid", name: "", schema: [ - { name: "name", selector: { text: {} } }, { name: "icon", selector: { diff --git a/src/panels/lovelace/editor/structs/entities-struct.ts b/src/panels/lovelace/editor/structs/entities-struct.ts index c0bb7aa4a7..20aa15bf88 100644 --- a/src/panels/lovelace/editor/structs/entities-struct.ts +++ b/src/panels/lovelace/editor/structs/entities-struct.ts @@ -4,11 +4,12 @@ import { actionConfigStruct, actionConfigStructConfirmation, } from "./action-struct"; +import { entityNameStruct } from "./entity-name-struct"; export const entitiesConfigStruct = union([ object({ entity: string(), - name: optional(string()), + name: optional(entityNameStruct), icon: optional(string()), image: optional(string()), secondary_info: optional(string()), diff --git a/src/panels/lovelace/editor/types.ts b/src/panels/lovelace/editor/types.ts index 30878a3540..1df3144445 100644 --- a/src/panels/lovelace/editor/types.ts +++ b/src/panels/lovelace/editor/types.ts @@ -43,9 +43,10 @@ export interface ConfigError { message: string; } -export interface EntitiesEditorEvent extends CustomEvent { +export interface EntitiesEditorEvent + extends CustomEvent { detail: { - entities?: EntityConfig[]; + entities?: T[]; item?: any; }; target: EventTarget | null; 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 74af3ef545..ea36acfdf4 100644 --- a/src/panels/lovelace/editor/unused-entities/hui-unused-entities.ts +++ b/src/panels/lovelace/editor/unused-entities/hui-unused-entities.ts @@ -111,11 +111,7 @@ export class HuiUnusedEntities extends LitElement { } private _addToLovelaceView(): void { - const cardConfig = computeCards( - this.hass.states, - this._selectedEntities, - {} - ); + const cardConfig = computeCards(this.hass, this._selectedEntities, {}); const sectionConfig = computeSection(this._selectedEntities, {}); if (this.lovelace.config.views.length === 1) { diff --git a/src/panels/lovelace/entity-rows/hui-datetime-entity-row.ts b/src/panels/lovelace/entity-rows/hui-datetime-entity-row.ts index 20581cd63e..981f78b2e4 100644 --- a/src/panels/lovelace/entity-rows/hui-datetime-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-datetime-entity-row.ts @@ -1,17 +1,17 @@ +import { format } from "date-fns"; import type { PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import "../../../components/ha-date-input"; -import { format } from "date-fns"; -import { isUnavailableState, UNAVAILABLE } from "../../../data/entity"; +import "../../../components/ha-time-input"; import { setDateTimeValue } from "../../../data/datetime"; +import { isUnavailableState, UNAVAILABLE } from "../../../data/entity"; import type { HomeAssistant } from "../../../types"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import "../components/hui-generic-entity-row"; import { createEntityNotFoundWarning } from "../components/hui-warning"; import type { EntityConfig, LovelaceRow } from "./types"; -import "../../../components/ha-time-input"; -import { computeStateName } from "../../../common/entity/compute_state_name"; +import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; @customElement("hui-datetime-entity-row") class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { @@ -53,6 +53,12 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { const time = dateObj ? format(dateObj, "HH:mm:ss") : undefined; const date = dateObj ? format(dateObj, "yyyy-MM-dd") : undefined; + const name = computeLovelaceEntityName( + this.hass!, + stateObj, + this._config.name + ); + return html`
diff --git a/src/panels/lovelace/entity-rows/types.ts b/src/panels/lovelace/entity-rows/types.ts index a5ca00a3fc..dbf0248267 100644 --- a/src/panels/lovelace/entity-rows/types.ts +++ b/src/panels/lovelace/entity-rows/types.ts @@ -1,3 +1,4 @@ +import type { EntityNameItem } from "../../../common/entity/compute_entity_name_display"; import type { ActionConfig, ConfirmationRestrictionConfig, @@ -10,7 +11,7 @@ import type { TimestampRenderingFormat } from "../components/types"; export interface EntityConfig { entity: string; type?: string; - name?: string; + name?: string | EntityNameItem | EntityNameItem[]; icon?: string; image?: string; } diff --git a/src/panels/lovelace/special-rows/hui-button-row.ts b/src/panels/lovelace/special-rows/hui-button-row.ts index e268249eb3..b59741aa7e 100644 --- a/src/panels/lovelace/special-rows/hui-button-row.ts +++ b/src/panels/lovelace/special-rows/hui-button-row.ts @@ -2,15 +2,15 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, state } from "lit/decorators"; import { DOMAINS_TOGGLE } from "../../../common/const"; import { computeDomain } from "../../../common/entity/compute_domain"; -import { computeStateName } from "../../../common/entity/compute_state_name"; -import "../../../components/ha-state-icon"; import "../../../components/ha-button"; +import "../../../components/ha-state-icon"; +import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; import type { HomeAssistant } from "../../../types"; import { actionHandler } from "../common/directives/action-handler-directive"; +import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; import { handleAction } from "../common/handle-action"; import { hasAction } from "../common/has-action"; import type { ButtonRowConfig, LovelaceRow } from "../entity-rows/types"; -import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; @customElement("hui-button-row") export class HuiButtonRow extends LitElement implements LovelaceRow { @@ -49,8 +49,11 @@ export class HuiButtonRow extends LitElement implements LovelaceRow { ? this.hass.states[this._config.entity] : undefined; - const name = - this._config.name ?? (stateObj ? computeStateName(stateObj) : ""); + const name = computeLovelaceEntityName( + this.hass!, + stateObj, + this._config.name + ); return html` Date: Thu, 30 Oct 2025 12:12:23 +0000 Subject: [PATCH 07/66] Revert "Migrate dialog-device-registry-detail to ha-wa-dialog (#27668)" (#27716) This reverts commit 2a8d9356019c3c5c33b6c8a958a7d2c86df1d54e. --- .../dialog-device-registry-detail.ts | 57 +++++++------------ 1 file changed, 22 insertions(+), 35 deletions(-) diff --git a/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts b/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts index b943e25cf0..673f9cf9f3 100644 --- a/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts +++ b/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts @@ -5,8 +5,7 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name"; import "../../../../components/ha-alert"; import "../../../../components/ha-area-picker"; -import "../../../../components/ha-wa-dialog"; -import "../../../../components/ha-dialog-footer"; +import "../../../../components/ha-dialog"; import "../../../../components/ha-button"; import "../../../../components/ha-labels-picker"; import type { HaSwitch } from "../../../../components/ha-switch"; @@ -20,8 +19,6 @@ import type { DeviceRegistryDetailDialogParams } from "./show-dialog-device-regi class DialogDeviceRegistryDetail extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @state() private _open = false; - @state() private _nameByUser!: string; @state() private _error?: string; @@ -45,15 +42,10 @@ class DialogDeviceRegistryDetail extends LitElement { this._areaId = this._params.device.area_id || ""; this._labels = this._params.device.labels || []; this._disabledBy = this._params.device.disabled_by; - this._open = true; await this.updateComplete; } public closeDialog(): void { - this._open = false; - } - - private _dialogClosed(): void { this._error = ""; this._params = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); @@ -65,12 +57,10 @@ class DialogDeviceRegistryDetail extends LitElement { } const device = this._params.device; return html` -
${this._error @@ -78,7 +68,6 @@ class DialogDeviceRegistryDetail extends LitElement { : ""}
- - - - ${this.hass.localize("ui.common.cancel")} - - - ${this.hass.localize("ui.dialogs.device-registry-detail.update")} - - -
+ + ${this.hass.localize("ui.common.cancel")} + + + ${this.hass.localize("ui.dialogs.device-registry-detail.update")} + + `; } From 165a757f06644db52bd72ecdd5301fd039cb2930 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 30 Oct 2025 16:24:09 +0100 Subject: [PATCH 08/66] Revert entity naming in target picker chips (#27722) --- .../ha-target-picker-value-chip.ts | 29 ++++--------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/src/components/target-picker/ha-target-picker-value-chip.ts b/src/components/target-picker/ha-target-picker-value-chip.ts index 200d7dd817..957ab0e839 100644 --- a/src/components/target-picker/ha-target-picker-value-chip.ts +++ b/src/components/target-picker/ha-target-picker-value-chip.ts @@ -16,14 +16,10 @@ import memoizeOne from "memoize-one"; import { computeCssColor } from "../../common/color/compute-color"; import { hex2rgb } from "../../common/color/convert-color"; import { fireEvent } from "../../common/dom/fire_event"; -import { slugify } from "../../common/string/slugify"; -import { - computeDeviceName, - computeDeviceNameDisplay, -} from "../../common/entity/compute_device_name"; +import { computeDeviceNameDisplay } 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 { computeStateName } from "../../common/entity/compute_state_name"; +import { slugify } from "../../common/string/slugify"; import { getConfigEntry } from "../../data/config_entries"; import { labelsContext } from "../../data/context"; import { domainToName } from "../../data/integration"; @@ -172,23 +168,10 @@ export class HaTargetPickerValueChip extends LitElement { if (type === "entity") { this._setDomainName(computeDomain(itemId)); - const stateObject = this.hass.states[itemId]; - const entityName = computeEntityName( - stateObject, - this.hass.entities, - this.hass.devices - ); - const { device } = getEntityContext( - stateObject, - this.hass.entities, - this.hass.devices, - this.hass.areas, - this.hass.floors - ); - const deviceName = device ? computeDeviceName(device) : undefined; + const stateObj = this.hass.states[itemId]; return { - name: entityName || deviceName || itemId, - stateObject, + name: computeStateName(stateObj) || itemId, + stateObject: stateObj, }; } From 843d79eab4860f1be3156ae987759f79751d9a41 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 30 Oct 2025 18:17:38 +0100 Subject: [PATCH 09/66] Don't show tooltip for ha button menu in top bar (#27723) --- src/panels/lovelace/hui-root.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index 2f7342703d..0e8eda2a7f 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -321,6 +321,8 @@ class HUIRoot extends LitElement { .id="button-${index}" .path=${item.icon} slot="trigger" + .label=${label} + hide-title > ${item.subItems .filter((subItem) => subItem.visible) @@ -340,9 +342,6 @@ class HUIRoot extends LitElement { ` )} - - ${label} - ` : html` Date: Thu, 30 Oct 2025 18:16:49 +0100 Subject: [PATCH 10/66] Revert "Fix entities card size and add grid contstraints" (#27725) --- .../lovelace/cards/hui-entities-card.ts | 132 ++++++++---------- 1 file changed, 58 insertions(+), 74 deletions(-) diff --git a/src/panels/lovelace/cards/hui-entities-card.ts b/src/panels/lovelace/cards/hui-entities-card.ts index 5c0ec2b390..e26ccf6736 100644 --- a/src/panels/lovelace/cards/hui-entities-card.ts +++ b/src/panels/lovelace/cards/hui-entities-card.ts @@ -1,6 +1,6 @@ import type { PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { customElement, state } from "lit/decorators"; import { DOMAINS_TOGGLE } from "../../../common/const"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { computeDomain } from "../../../common/entity/compute_domain"; @@ -20,11 +20,9 @@ import type { import type { LovelaceCard, LovelaceCardEditor, - LovelaceGridOptions, LovelaceHeaderFooter, } from "../types"; import type { EntitiesCardConfig } from "./types"; -import { haStyleScrollbar } from "../../../resources/styles"; export const computeShowHeaderToggle = < T extends EntityConfig | LovelaceRowConfig, @@ -77,8 +75,6 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard { private _hass?: HomeAssistant; - @property({ attribute: false }) public layout?: string; - private _configEntities?: LovelaceRowConfig[]; private _showHeaderToggle?: boolean; @@ -143,14 +139,6 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard { return size; } - public getGridOptions(): LovelaceGridOptions { - return { - columns: 12, - min_columns: 6, - min_rows: this._config?.title || this._showHeaderToggle ? 3 : 2, - }; - } - public setConfig(config: EntitiesCardConfig): void { if (!config.entities || !Array.isArray(config.entities)) { throw new Error("Entities must be specified"); @@ -245,7 +233,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard { `} `} -
+
${this._configEntities!.map((entityConf) => this._renderEntity(entityConf) )} @@ -258,73 +246,69 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard { `; } - static styles = [ - haStyleScrollbar, - css` - ha-card { - height: 100%; - display: flex; - flex-direction: column; - justify-content: space-between; - } - .card-header { - display: flex; - justify-content: space-between; - } + static styles = css` + ha-card { + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + } + .card-header { + display: flex; + justify-content: space-between; + } - .card-header .name { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } + .card-header .name { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } - #states { - flex: 1; - display: flex; - flex-direction: column; - gap: var(--entities-card-row-gap, var(--card-row-gap, 8px)); - overflow-y: auto; - } + #states { + flex: 1; + display: flex; + flex-direction: column; + gap: var(--entities-card-row-gap, var(--card-row-gap, 8px)); + } - #states > div > * { - overflow: clip visible; - } + #states > div > * { + overflow: clip visible; + } - #states > div { - position: relative; - } + #states > div { + position: relative; + } - .icon { - padding: 0px 18px 0px 8px; - } + .icon { + padding: 0px 18px 0px 8px; + } - .header { - border-top-left-radius: var( - --ha-card-border-radius, - var(--ha-border-radius-lg) - ); - border-top-right-radius: var( - --ha-card-border-radius, - var(--ha-border-radius-lg) - ); - margin-bottom: 16px; - overflow: hidden; - } + .header { + border-top-left-radius: var( + --ha-card-border-radius, + var(--ha-border-radius-lg) + ); + border-top-right-radius: var( + --ha-card-border-radius, + var(--ha-border-radius-lg) + ); + margin-bottom: 16px; + overflow: hidden; + } - .footer { - border-bottom-left-radius: var( - --ha-card-border-radius, - var(--ha-border-radius-lg) - ); - border-bottom-right-radius: var( - --ha-card-border-radius, - var(--ha-border-radius-lg) - ); - margin-top: -16px; - overflow: hidden; - } - `, - ]; + .footer { + border-bottom-left-radius: var( + --ha-card-border-radius, + var(--ha-border-radius-lg) + ); + border-bottom-right-radius: var( + --ha-card-border-radius, + var(--ha-border-radius-lg) + ); + margin-top: -16px; + overflow: hidden; + } + `; private _renderEntity(entityConf: LovelaceRowConfig): TemplateResult { const element = createRowElement( From 0408734ec568f10595d8b00603baa095938bae3b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 30 Oct 2025 18:19:31 +0100 Subject: [PATCH 11/66] Bumped version to 20251029.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 66a7cb314d..fca8e10e87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20251029.0" +version = "20251029.1" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend" From 6e6e5a53e2ba13405e65eee5a359432568afa33f Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sat, 1 Nov 2025 10:38:33 +0100 Subject: [PATCH 12/66] Fix button text overflow (#27744) --- src/components/ha-button.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ha-button.ts b/src/components/ha-button.ts index 7d286f8ffb..7f678e955d 100644 --- a/src/components/ha-button.ts +++ b/src/components/ha-button.ts @@ -59,6 +59,7 @@ export class HaButton extends Button { line-height: 1; transition: background-color 0.15s ease-in-out; + text-wrap: wrap; } :host([size="small"]) .button { From 8e1b6a3d3b581719b6ff4e20faed159c2704cf54 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:26:42 +0100 Subject: [PATCH 13/66] Fixes in backup overflow (#27745) Co-authored-by: Bram Kragten --- .../config/backup/ha-config-backup-backups.ts | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/panels/config/backup/ha-config-backup-backups.ts b/src/panels/config/backup/ha-config-backup-backups.ts index 67a2965050..382abe6a1f 100644 --- a/src/panels/config/backup/ha-config-backup-backups.ts +++ b/src/panels/config/backup/ha-config-backup-backups.ts @@ -125,8 +125,6 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { @query("#overflow-menu") private _overflowMenu?: HaMdMenu; - private _overflowBackup?: BackupContent; - public connectedCallback() { super.connectedCallback(); window.addEventListener("location-changed", this._locationChanged); @@ -262,7 +260,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { type: "overflow-menu", template: (backup) => html` { - if (!this._overflowBackup) { + private async _downloadBackup(ev): Promise { + const backup = ev.parentElement.anchorElement.backup; + if (!backup) { return; } - downloadBackup(this.hass, this, this._overflowBackup, this.config); + downloadBackup(this.hass, this, backup, this.config); } - private async _deleteBackup(): Promise { - if (!this._overflowBackup) { + private async _deleteBackup(ev): Promise { + const backup = ev.parentElement.anchorElement.backup; + if (!backup) { return; } @@ -596,11 +595,9 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { } try { - await deleteBackup(this.hass, this._overflowBackup.backup_id); - if (this._selected.includes(this._overflowBackup.backup_id)) { - this._selected = this._selected.filter( - (id) => id !== this._overflowBackup!.backup_id - ); + await deleteBackup(this.hass, backup.backup_id); + if (this._selected.includes(backup.backup_id)) { + this._selected = this._selected.filter((id) => id !== backup.backup_id); } } catch (err: any) { showAlertDialog(this, { From 7f9a9de15783bdf5c7a3be9b1c041c3588df6000 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Mon, 3 Nov 2025 07:20:12 +0100 Subject: [PATCH 14/66] Move label translations to ui.dialog (#27752) --- .../config/labels/dialog-label-detail.ts | 24 +++++++------------ src/translations/en.json | 21 +++++++--------- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/src/panels/config/labels/dialog-label-detail.ts b/src/panels/config/labels/dialog-label-detail.ts index 916d99f45d..63d9cbd485 100644 --- a/src/panels/config/labels/dialog-label-detail.ts +++ b/src/panels/config/labels/dialog-label-detail.ts @@ -82,7 +82,7 @@ class DialogLabelDetail this.hass, this._params.entry ? this._params.entry.name || this._params.entry.label_id - : this.hass!.localize("ui.panel.config.labels.detail.new_label") + : this.hass!.localize("ui.dialogs.label-detail.new_label") )} >
@@ -95,11 +95,9 @@ class DialogLabelDetail .value=${this._name} .configValue=${"name"} @input=${this._input} - .label=${this.hass!.localize( - "ui.panel.config.labels.detail.name" - )} + .label=${this.hass!.localize("ui.dialogs.label-detail.name")} .validationMessage=${this.hass!.localize( - "ui.panel.config.labels.detail.required_error_msg" + "ui.dialogs.label-detail.required_error_msg" )} required > @@ -108,25 +106,21 @@ class DialogLabelDetail .hass=${this.hass} .configValue=${"icon"} @value-changed=${this._valueChanged} - .label=${this.hass!.localize( - "ui.panel.config.labels.detail.icon" - )} + .label=${this.hass!.localize("ui.dialogs.label-detail.icon")} >
@@ -140,7 +134,7 @@ class DialogLabelDetail @click=${this._deleteEntry} .disabled=${this._submitting} > - ${this.hass!.localize("ui.panel.config.labels.detail.delete")} + ${this.hass!.localize("ui.common.delete")} ` : nothing} @@ -150,8 +144,8 @@ class DialogLabelDetail .disabled=${this._submitting || !this._name} > ${this._params.entry - ? this.hass!.localize("ui.panel.config.labels.detail.update") - : this.hass!.localize("ui.panel.config.labels.detail.create")} + ? this.hass!.localize("ui.common.update") + : this.hass!.localize("ui.common.create")} `; diff --git a/src/translations/en.json b/src/translations/en.json index 1bdcfbc370..293ebb81b7 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1706,6 +1706,14 @@ "update": "[%key:ui::panel::config::devices::update%]", "unknown_error": "[%key:ui::panel::config::devices::unknown_error%]" }, + "label-detail": { + "new_label": "New label", + "name": "Name", + "icon": "Icon", + "color": "Color", + "description": "Description", + "required_error_msg": "[%key:ui::panel::config::zone::detail::required_error_msg%]" + }, "voice-settings": { "expose_header": "Expose", "aliases_header": "Aliases", @@ -2406,18 +2414,7 @@ "introduction": "Labels can help you organize your areas, devices, and entities. They can be used to filter in the UI, or use them as a target in automations.", "introduction2": "Go to the area, device, or entity you want to add a label to, and press the edit button to assign labels to them.", "confirm_remove_title": "Remove label?", - "confirm_remove": "Are you sure you want to remove label {label}? It will be removed from all areas, devices, and entities.", - "detail": { - "new_label": "New label", - "name": "Name", - "icon": "Icon", - "color": "Color", - "description": "Description", - "delete": "Delete", - "update": "Update", - "create": "Create", - "required_error_msg": "[%key:ui::panel::config::zone::detail::required_error_msg%]" - } + "confirm_remove": "Are you sure you want to remove label {label}? It will be removed from all areas, devices, and entities." }, "areas": { "caption": "Areas", From feb68ce373530562100edcdc141c3b0ae1cc4650 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Sun, 2 Nov 2025 18:54:40 +0200 Subject: [PATCH 15/66] Add support for PM4 sensor state (#27754) --- gallery/src/pages/misc/entity-state.ts | 1 + src/common/entity/get_states.ts | 1 + src/fake_data/entity_component_icons.ts | 6 ++++++ test/common/entity/get_states.test.ts | 3 ++- 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/gallery/src/pages/misc/entity-state.ts b/gallery/src/pages/misc/entity-state.ts index 8d1fa8a020..832fdaba02 100644 --- a/gallery/src/pages/misc/entity-state.ts +++ b/gallery/src/pages/misc/entity-state.ts @@ -39,6 +39,7 @@ const SENSOR_DEVICE_CLASSES = [ "pm1", "pm10", "pm25", + "pm4", "power_factor", "power", "precipitation", diff --git a/src/common/entity/get_states.ts b/src/common/entity/get_states.ts index 9fb55b02a2..8f6f6c8fd8 100644 --- a/src/common/entity/get_states.ts +++ b/src/common/entity/get_states.ts @@ -214,6 +214,7 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = { "pm1", "pm10", "pm25", + "pm4", "power_factor", "power", "pressure", diff --git a/src/fake_data/entity_component_icons.ts b/src/fake_data/entity_component_icons.ts index ab59081d8c..2b669ca191 100644 --- a/src/fake_data/entity_component_icons.ts +++ b/src/fake_data/entity_component_icons.ts @@ -97,6 +97,9 @@ export const ENTITY_COMPONENT_ICONS: Record = { pm25: { default: "mdi:molecule", }, + pm4: { + default: "mdi:molecule", + }, power: { default: "mdi:flash", }, @@ -674,6 +677,9 @@ export const ENTITY_COMPONENT_ICONS: Record = { pm25: { default: "mdi:molecule", }, + pm4: { + default: "mdi:molecule", + }, power: { default: "mdi:flash", }, diff --git a/test/common/entity/get_states.test.ts b/test/common/entity/get_states.test.ts index ba3a8c0f71..0f3b60fd4d 100644 --- a/test/common/entity/get_states.test.ts +++ b/test/common/entity/get_states.test.ts @@ -201,6 +201,7 @@ describe("getStates", () => { "pm1", "pm10", "pm25", + "pm4", "power_factor", "power", "pressure", @@ -215,7 +216,7 @@ describe("getStates", () => { "volume_flow_rate", ]) ); - expect(result.length).toBe(34); + expect(result.length).toBe(35); }); it("should return empty array for unknown attribute", () => { From 11d3f5c2ba643ae2ba81b980582761735c16db00 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 3 Nov 2025 09:56:52 +0100 Subject: [PATCH 16/66] Fix suggest cards dialog for sections view (#27762) --- .../lovelace/editor/card-editor/hui-dialog-suggest-card.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts index 3b9f55167d..628546c997 100644 --- a/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts @@ -17,6 +17,7 @@ import type { HomeAssistant } from "../../../../types"; import { showSaveSuccessToast } from "../../../../util/toast-saved-success"; import "../../cards/hui-card"; import "../../sections/hui-section"; +import { getViewType } from "../../views/get-view-type"; import { addCards, addSection } from "../config-util"; import type { LovelaceContainerPath } from "../lovelace-path"; import { parseLovelaceContainerPath } from "../lovelace-path"; @@ -66,7 +67,9 @@ export class HuiDialogSuggestCard extends LitElement { const { viewIndex } = parseLovelaceContainerPath(this._params.path); const viewConfig = this._params!.lovelaceConfig.views[viewIndex]; - return !isStrategyView(viewConfig) && viewConfig.type === "sections"; + return ( + !isStrategyView(viewConfig) && getViewType(viewConfig) === "sections" + ); } private _renderPreview() { From ee6c82aba9dcc0a3edfa1ef1fd200c0b94c34ee1 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 3 Nov 2025 14:30:04 +0100 Subject: [PATCH 17/66] Don't show tooltip on overflow menu in dashboard toolbar (#27763) --- src/panels/lovelace/hui-root.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index 0e8eda2a7f..7308337a53 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -391,12 +391,11 @@ class HUIRoot extends LitElement { slot="trigger" id="dashboardmenu" .path=${mdiDotsVertical} + .label=${this.hass!.localize("ui.panel.lovelace.editor.menu.open")} + hide-title >
${listItems} - - ${this.hass!.localize("ui.panel.lovelace.editor.menu.open")} - `); } return html`${result}`; From fdc9f5a3b761edde3dbf68c0d9a559cac9b1c0f1 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 3 Nov 2025 14:58:29 +0000 Subject: [PATCH 18/66] Use supervisor endpoint for downloading logs (when avaliable) (#27765) --- src/data/error_log.ts | 8 +++++++- .../config/integrations/ha-config-integration-page.ts | 5 ++++- src/panels/config/logs/error-log-card.ts | 2 +- src/panels/config/logs/system-log-card.ts | 9 +-------- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/data/error_log.ts b/src/data/error_log.ts index a1628cc014..410a6eb80a 100644 --- a/src/data/error_log.ts +++ b/src/data/error_log.ts @@ -1,3 +1,5 @@ +import { isComponentLoaded } from "../common/config/is_component_loaded"; +import { atLeastVersion } from "../common/config/version"; import type { HomeAssistant } from "../types"; export interface LogProvider { @@ -8,4 +10,8 @@ export interface LogProvider { export const fetchErrorLog = (hass: HomeAssistant) => hass.callApi("GET", "error_log"); -export const getErrorLogDownloadUrl = "/api/error_log"; +export const getErrorLogDownloadUrl = (hass: HomeAssistant) => + isComponentLoaded(hass, "hassio") && + atLeastVersion(hass.config.version, 2025, 10) + ? "/api/hassio/core/logs/latest" + : "/api/error_log"; diff --git a/src/panels/config/integrations/ha-config-integration-page.ts b/src/panels/config/integrations/ha-config-integration-page.ts index eb3d97e7b3..2a72b73da6 100644 --- a/src/panels/config/integrations/ha-config-integration-page.ts +++ b/src/panels/config/integrations/ha-config-integration-page.ts @@ -790,7 +790,10 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) { ); const timeString = new Date().toISOString().replace(/:/g, "-"); const logFileName = `home-assistant_${integration}_${timeString}.log`; - const signedUrl = await getSignedPath(this.hass, getErrorLogDownloadUrl); + const signedUrl = await getSignedPath( + this.hass, + getErrorLogDownloadUrl(this.hass) + ); fileDownload(signedUrl.path, logFileName); } diff --git a/src/panels/config/logs/error-log-card.ts b/src/panels/config/logs/error-log-card.ts index d5f980576e..1c0e742240 100644 --- a/src/panels/config/logs/error-log-card.ts +++ b/src/panels/config/logs/error-log-card.ts @@ -415,7 +415,7 @@ class ErrorLogCard extends LitElement { const downloadUrl = this.provider && this.provider !== "core" ? getHassioLogDownloadUrl(this.provider) - : getErrorLogDownloadUrl; + : getErrorLogDownloadUrl(this.hass); const logFileName = this.provider && this.provider !== "core" ? `${this.provider}_${timeString}.log` diff --git a/src/panels/config/logs/system-log-card.ts b/src/panels/config/logs/system-log-card.ts index d53a923cb3..6c858c7f99 100644 --- a/src/panels/config/logs/system-log-card.ts +++ b/src/panels/config/logs/system-log-card.ts @@ -2,8 +2,6 @@ import { mdiDotsVertical, mdiDownload, mdiRefresh, mdiText } from "@mdi/js"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; -import { isComponentLoaded } from "../../../common/config/is_component_loaded"; -import { atLeastVersion } from "../../../common/config/version"; import { fireEvent } from "../../../common/dom/fire_event"; import type { LocalizeFunc } from "../../../common/translations/localize"; import "../../../components/buttons/ha-call-service-button"; @@ -15,7 +13,6 @@ import "../../../components/ha-list-item"; import "../../../components/ha-spinner"; import { getSignedPath } from "../../../data/auth"; import { getErrorLogDownloadUrl } from "../../../data/error_log"; -import { coreLatestLogsUrl } from "../../../data/hassio/supervisor"; import { domainToName } from "../../../data/integration"; import type { LoggedError } from "../../../data/system_log"; import { @@ -231,11 +228,7 @@ export class SystemLogCard extends LitElement { private async _downloadLogs() { const timeString = new Date().toISOString().replace(/:/g, "-"); - const downloadUrl = - isComponentLoaded(this.hass, "hassio") && - atLeastVersion(this.hass.config.version, 2025, 10) - ? coreLatestLogsUrl - : getErrorLogDownloadUrl; + const downloadUrl = getErrorLogDownloadUrl(this.hass); const logFileName = `home-assistant_${timeString}.log`; const signedUrl = await getSignedPath(this.hass, downloadUrl); fileDownload(signedUrl.path, logFileName); From 82d44e051f0046de37c17c19d567a06362361567 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 3 Nov 2025 17:00:06 +0200 Subject: [PATCH 19/66] Fix sensor card graph in Safari (#27768) --- src/panels/lovelace/header-footer/hui-graph-header-footer.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/panels/lovelace/header-footer/hui-graph-header-footer.ts b/src/panels/lovelace/header-footer/hui-graph-header-footer.ts index b45ba3c931..9ce291f3d2 100644 --- a/src/panels/lovelace/header-footer/hui-graph-header-footer.ts +++ b/src/panels/lovelace/header-footer/hui-graph-header-footer.ts @@ -218,6 +218,9 @@ export class HuiGraphHeaderFooter } static styles = css` + :host { + display: block; + } ha-spinner { position: absolute; top: calc(50% - 14px); From d76781eb91776c416153fcfc97730c9031a7187e Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 3 Nov 2025 16:59:46 +0200 Subject: [PATCH 20/66] Fix for Y axis label formatting in history graph (#27770) --- src/components/chart/state-history-chart-line.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index dec261e3aa..99084c0d52 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -87,6 +87,8 @@ export class StateHistoryChartLine extends LitElement { private _previousYAxisLabelValue = 0; + private _yAxisMaximumFractionDigits = 0; + protected render() { return html` this._yWidth) { From 1eda44a1023799ab4f3650ca8fa32bf79ad9a9b7 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:57:01 +0100 Subject: [PATCH 21/66] Fix selected element text color (#27771) --- .../config/automation/add-automation-element-dialog.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/panels/config/automation/add-automation-element-dialog.ts b/src/panels/config/automation/add-automation-element-dialog.ts index 25d698dab7..bb946abeff 100644 --- a/src/panels/config/automation/add-automation-element-dialog.ts +++ b/src/panels/config/automation/add-automation-element-dialog.ts @@ -1137,11 +1137,11 @@ class DialogAddAutomationElement } .groups .selected { background-color: var(--ha-color-fill-primary-normal-active); - --md-list-item-label-text-color: var(--primary-color); - --icon-primary-color: var(--primary-color); + --md-list-item-label-text-color: var(--ha-color-on-primary-normal); + --icon-primary-color: var(--ha-color-on-primary-normal); } .groups .selected ha-svg-icon { - color: var(--primary-color); + color: var(--ha-color-on-primary-normal); } .collection-title { From 95311be0343ee32fb75f398d5532b7d95f3ee0cd Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 3 Nov 2025 17:27:09 +0200 Subject: [PATCH 22/66] Apply theme variables to pi charts (#27773) --- src/components/chart/ha-chart-base.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 4770646c1e..f4d8f31c93 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -652,6 +652,13 @@ export class HaChartBase extends LitElement { textBorderWidth: 2, }, }, + pie: { + label: { + color: style.getPropertyValue("--primary-text-color"), + textBorderColor: style.getPropertyValue("--primary-background-color"), + textBorderWidth: 2, + }, + }, sankey: { label: { color: style.getPropertyValue("--primary-text-color"), From 561122f03d475af1897425eb970e8b7269d41ba7 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 3 Nov 2025 16:34:11 +0100 Subject: [PATCH 23/66] Bumped version to 20251103.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fca8e10e87..6769fc6f66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20251029.1" +version = "20251103.0" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend" From 0c35278f51459651719792dafc36761b9b9d1afa Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 4 Nov 2025 11:44:12 +0100 Subject: [PATCH 24/66] Hide media players summary when no entities exist (#27642) --- .../home/home-main-view-strategy.ts | 129 ++++++++++-------- 1 file changed, 71 insertions(+), 58 deletions(-) diff --git a/src/panels/lovelace/strategies/home/home-main-view-strategy.ts b/src/panels/lovelace/strategies/home/home-main-view-strategy.ts index c3364334d2..f0b3d32b88 100644 --- a/src/panels/lovelace/strategies/home/home-main-view-strategy.ts +++ b/src/panels/lovelace/strategies/home/home-main-view-strategy.ts @@ -144,67 +144,80 @@ export class HomeMainViewStrategy extends ReactiveElement { column_span: maxColumns, } as LovelaceStrategySectionConfig; + // Check if there are any media player entities + const mediaPlayerFilter = generateEntityFilter(hass, { + domain: "media_player", + entity_category: "none", + }); + const hasMediaPlayers = Object.keys(hass.states).some(mediaPlayerFilter); + + const summaryCards: LovelaceCardConfig[] = [ + { + type: "heading", + heading: hass.localize("ui.panel.lovelace.strategy.home.summaries"), + }, + { + type: "home-summary", + summary: "light", + vertical: true, + tap_action: { + action: "navigate", + navigation_path: "/light?historyBack=1", + }, + grid_options: { + rows: 2, + columns: 4, + }, + } satisfies HomeSummaryCard, + { + type: "home-summary", + summary: "climate", + vertical: true, + tap_action: { + action: "navigate", + navigation_path: "/climate?historyBack=1", + }, + grid_options: { + rows: 2, + columns: 4, + }, + } satisfies HomeSummaryCard, + { + type: "home-summary", + summary: "safety", + vertical: true, + tap_action: { + action: "navigate", + navigation_path: "/safety?historyBack=1", + }, + grid_options: { + rows: 2, + columns: 4, + }, + } satisfies HomeSummaryCard, + ]; + + // Only add media players summary if there are media player entities + if (hasMediaPlayers) { + summaryCards.push({ + type: "home-summary", + summary: "media_players", + vertical: true, + tap_action: { + action: "navigate", + navigation_path: "media-players", + }, + grid_options: { + rows: 2, + columns: 4, + }, + } satisfies HomeSummaryCard); + } + const summarySection: LovelaceSectionConfig = { type: "grid", column_span: maxColumns, - cards: [ - { - type: "heading", - heading: hass.localize("ui.panel.lovelace.strategy.home.summaries"), - }, - { - type: "home-summary", - summary: "light", - vertical: true, - tap_action: { - action: "navigate", - navigation_path: "/light?historyBack=1", - }, - grid_options: { - rows: 2, - columns: 4, - }, - } satisfies HomeSummaryCard, - { - type: "home-summary", - summary: "climate", - vertical: true, - tap_action: { - action: "navigate", - navigation_path: "/climate?historyBack=1", - }, - grid_options: { - rows: 2, - columns: 4, - }, - } satisfies HomeSummaryCard, - { - type: "home-summary", - summary: "safety", - vertical: true, - tap_action: { - action: "navigate", - navigation_path: "/safety?historyBack=1", - }, - grid_options: { - rows: 2, - columns: 4, - }, - } satisfies HomeSummaryCard, - { - type: "home-summary", - summary: "media_players", - vertical: true, - tap_action: { - action: "navigate", - navigation_path: "media-players", - }, - grid_options: { - rows: 2, - columns: 4, - }, - } satisfies HomeSummaryCard, - ], + cards: summaryCards, }; const weatherFilter = generateEntityFilter(hass, { From 7ebdeab6b27294addb52d5a772c064442a421e36 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Tue, 4 Nov 2025 10:01:36 +0100 Subject: [PATCH 25/66] Fix-labels-yaml-helper (#27776) Co-authored-by: Bram Kragten --- .../settings/entity-settings-helper-tab.ts | 37 +++++++++++++------ .../entity-registry-settings-editor.ts | 2 +- .../config/helpers/forms/ha-counter-form.ts | 9 +++++ .../helpers/forms/ha-input_boolean-form.ts | 4 ++ .../helpers/forms/ha-input_button-form.ts | 4 ++ .../helpers/forms/ha-input_datetime-form.ts | 7 ++++ .../helpers/forms/ha-input_number-form.ts | 10 +++++ .../helpers/forms/ha-input_select-form.ts | 18 ++++++++- .../helpers/forms/ha-input_text-form.ts | 8 ++++ .../config/helpers/forms/ha-schedule-form.ts | 12 ++++-- .../config/helpers/forms/ha-timer-form.ts | 17 +++++++-- 11 files changed, 106 insertions(+), 22 deletions(-) diff --git a/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts b/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts index d63f58ce05..e2b0009362 100644 --- a/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts +++ b/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts @@ -4,6 +4,7 @@ import { customElement, property, query, state } from "lit/decorators"; import { isComponentLoaded } from "../../../../../common/config/is_component_loaded"; import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../../../../common/dom/fire_event"; +import "../../../../../components/ha-button"; import type { ExtEntityRegistryEntry } from "../../../../../data/entity_registry"; import { removeEntityRegistryEntry } from "../../../../../data/entity_registry"; import { HELPERS_CRUD } from "../../../../../data/helpers_crud"; @@ -22,7 +23,6 @@ import "../../../helpers/forms/ha-schedule-form"; import "../../../helpers/forms/ha-timer-form"; import "../../../voice-assistants/entity-voice-settings"; import "../../entity-registry-settings-editor"; -import "../../../../../components/ha-button"; import type { EntityRegistrySettingsEditor } from "../../entity-registry-settings-editor"; @customElement("entity-settings-helper-tab") @@ -72,22 +72,28 @@ export class EntitySettingsHelperTab extends LitElement { ${this._error ? html`${this._error}` : ""} + ${this._item === null + ? html`${this.hass.localize( + "ui.dialogs.helper_settings.yaml_not_editable" + )}` + : nothing} ${!this._componentLoaded ? this.hass.localize( "ui.dialogs.helper_settings.platform_not_loaded", { platform: this.entry.platform } ) - : this._item === null - ? this.hass.localize("ui.dialogs.helper_settings.yaml_not_editable") - : html` - - ${dynamicElement(`ha-${this.entry.platform}-form`, { - hass: this.hass, - item: this._item, - entry: this.entry, - })} - - `} + : html` + + ${dynamicElement(`ha-${this.entry.platform}-form`, { + hass: this.hass, + item: this._item, + entry: this.entry, + disabled: this._item === null, + })} + + `} ${this._cameraPrefs diff --git a/src/panels/config/helpers/forms/ha-counter-form.ts b/src/panels/config/helpers/forms/ha-counter-form.ts index 79d6d3f2c4..3f289d673b 100644 --- a/src/panels/config/helpers/forms/ha-counter-form.ts +++ b/src/panels/config/helpers/forms/ha-counter-form.ts @@ -17,6 +17,8 @@ class HaCounterForm extends LitElement { @property({ type: Boolean }) public new = false; + @property({ type: Boolean }) public disabled = false; + private _item?: Partial; @state() private _name!: string; @@ -82,6 +84,7 @@ class HaCounterForm extends LitElement { "ui.dialogs.helper_settings.required_error_msg" )} dialogInitialFocus + .disabled=${this.disabled} >
diff --git a/src/panels/config/helpers/forms/ha-input_boolean-form.ts b/src/panels/config/helpers/forms/ha-input_boolean-form.ts index 51291300ae..221c258f5c 100644 --- a/src/panels/config/helpers/forms/ha-input_boolean-form.ts +++ b/src/panels/config/helpers/forms/ha-input_boolean-form.ts @@ -14,6 +14,8 @@ class HaInputBooleanForm extends LitElement { @property({ type: Boolean }) public new = false; + @property({ type: Boolean }) public disabled = false; + private _item?: InputBoolean; @state() private _name!: string; @@ -59,6 +61,7 @@ class HaInputBooleanForm extends LitElement { "ui.dialogs.helper_settings.required_error_msg" )} dialogInitialFocus + .disabled=${this.disabled} >
`; diff --git a/src/panels/config/helpers/forms/ha-input_button-form.ts b/src/panels/config/helpers/forms/ha-input_button-form.ts index 3ab2addb86..2f07f4a1d3 100644 --- a/src/panels/config/helpers/forms/ha-input_button-form.ts +++ b/src/panels/config/helpers/forms/ha-input_button-form.ts @@ -14,6 +14,8 @@ class HaInputButtonForm extends LitElement { @property({ type: Boolean }) public new = false; + @property({ type: Boolean }) public disabled = false; + @state() private _name!: string; @state() private _icon!: string; @@ -59,6 +61,7 @@ class HaInputButtonForm extends LitElement { "ui.dialogs.helper_settings.required_error_msg" )} dialogInitialFocus + .disabled=${this.disabled} >
`; diff --git a/src/panels/config/helpers/forms/ha-input_datetime-form.ts b/src/panels/config/helpers/forms/ha-input_datetime-form.ts index 23056b4ade..f808108001 100644 --- a/src/panels/config/helpers/forms/ha-input_datetime-form.ts +++ b/src/panels/config/helpers/forms/ha-input_datetime-form.ts @@ -17,6 +17,8 @@ class HaInputDateTimeForm extends LitElement { @property({ type: Boolean }) public new = false; + @property({ type: Boolean }) public disabled = false; + private _item?: InputDateTime; @state() private _name!: string; @@ -73,6 +75,7 @@ class HaInputDateTimeForm extends LitElement { "ui.dialogs.helper_settings.required_error_msg" )} dialogInitialFocus + .disabled=${this.disabled} >
${this.hass.localize("ui.dialogs.helper_settings.input_datetime.mode")}: @@ -97,6 +101,7 @@ class HaInputDateTimeForm extends LitElement { value="date" .checked=${this._mode === "date"} @change=${this._modeChanged} + .disabled=${this.disabled} >
diff --git a/src/panels/config/helpers/forms/ha-input_number-form.ts b/src/panels/config/helpers/forms/ha-input_number-form.ts index 7444136ba2..c937e494f6 100644 --- a/src/panels/config/helpers/forms/ha-input_number-form.ts +++ b/src/panels/config/helpers/forms/ha-input_number-form.ts @@ -18,6 +18,8 @@ class HaInputNumberForm extends LitElement { @property({ type: Boolean }) public new = false; + @property({ type: Boolean }) public disabled = false; + private _item?: Partial; @state() private _name!: string; @@ -89,6 +91,7 @@ class HaInputNumberForm extends LitElement { "ui.dialogs.helper_settings.required_error_msg" )} dialogInitialFocus + .disabled=${this.disabled} >
@@ -163,6 +171,7 @@ class HaInputNumberForm extends LitElement { .label=${this.hass!.localize( "ui.dialogs.helper_settings.input_number.step" )} + .disabled=${this.disabled} >
diff --git a/src/panels/config/helpers/forms/ha-input_select-form.ts b/src/panels/config/helpers/forms/ha-input_select-form.ts index 2529db4ee0..b4de226113 100644 --- a/src/panels/config/helpers/forms/ha-input_select-form.ts +++ b/src/panels/config/helpers/forms/ha-input_select-form.ts @@ -23,6 +23,8 @@ class HaInputSelectForm extends LitElement { @property({ type: Boolean }) public new = false; + @property({ type: Boolean }) public disabled = false; + private _item?: InputSelect; @state() private _name!: string; @@ -86,6 +88,7 @@ class HaInputSelectForm extends LitElement { )} .configValue=${"name"} @input=${this._valueChanged} + .disabled=${this.disabled} >
${this.hass!.localize( "ui.dialogs.helper_settings.input_select.options" )}:
- + ${this._options.length ? repeat( @@ -124,6 +132,7 @@ class HaInputSelectForm extends LitElement { "ui.dialogs.helper_settings.input_select.remove_option" )} @click=${this._removeOption} + .disabled=${this.disabled} .path=${mdiDelete} > @@ -146,8 +155,13 @@ class HaInputSelectForm extends LitElement { "ui.dialogs.helper_settings.input_select.add_option" )} @keydown=${this._handleKeyAdd} + .disabled=${this.disabled} > - ${this.hass!.localize( "ui.dialogs.helper_settings.input_select.add" )}
@@ -154,6 +161,7 @@ class HaInputTextForm extends LitElement { .helper=${this.hass!.localize( "ui.dialogs.helper_settings.input_text.pattern_helper" )} + .disabled=${this.disabled} >
diff --git a/src/panels/config/helpers/forms/ha-schedule-form.ts b/src/panels/config/helpers/forms/ha-schedule-form.ts index 6bccf9c716..13b593888d 100644 --- a/src/panels/config/helpers/forms/ha-schedule-form.ts +++ b/src/panels/config/helpers/forms/ha-schedule-form.ts @@ -17,9 +17,9 @@ import "../../../../components/ha-textfield"; import type { Schedule, ScheduleDay } from "../../../../data/schedule"; import { weekdays } from "../../../../data/schedule"; import { TimeZone } from "../../../../data/translation"; -import { showScheduleBlockInfoDialog } from "./show-dialog-schedule-block-info"; import { haStyle } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; +import { showScheduleBlockInfoDialog } from "./show-dialog-schedule-block-info"; const defaultFullCalendarConfig: CalendarOptions = { plugins: [timeGridPlugin, interactionPlugin], @@ -43,6 +43,8 @@ class HaScheduleForm extends LitElement { @property({ type: Boolean }) public new = false; + @property({ type: Boolean }) public disabled = false; + @state() private _name!: string; @state() private _icon!: string; @@ -132,6 +134,7 @@ class HaScheduleForm extends LitElement { "ui.dialogs.helper_settings.required_error_msg" )} dialogInitialFocus + .disabled=${this.disabled} > -
+ ${!this.disabled ? html`
` : nothing}
`; } @@ -175,7 +179,9 @@ class HaScheduleForm extends LitElement { } protected firstUpdated(): void { - this._setupCalendar(); + if (!this.disabled) { + this._setupCalendar(); + } } private _setupCalendar(): void { diff --git a/src/panels/config/helpers/forms/ha-timer-form.ts b/src/panels/config/helpers/forms/ha-timer-form.ts index 236681051d..86bd916df2 100644 --- a/src/panels/config/helpers/forms/ha-timer-form.ts +++ b/src/panels/config/helpers/forms/ha-timer-form.ts @@ -1,18 +1,18 @@ import type { CSSResultGroup } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { createDurationData } from "../../../../common/datetime/create_duration_data"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-checkbox"; +import "../../../../components/ha-duration-input"; +import type { HaDurationData } from "../../../../components/ha-duration-input"; import "../../../../components/ha-formfield"; import "../../../../components/ha-icon-picker"; -import "../../../../components/ha-duration-input"; import "../../../../components/ha-textfield"; +import type { ForDict } from "../../../../data/automation"; import type { DurationDict, Timer } from "../../../../data/timer"; import { haStyle } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; -import { createDurationData } from "../../../../common/datetime/create_duration_data"; -import type { HaDurationData } from "../../../../components/ha-duration-input"; -import type { ForDict } from "../../../../data/automation"; @customElement("ha-timer-form") class HaTimerForm extends LitElement { @@ -20,6 +20,8 @@ class HaTimerForm extends LitElement { @property({ type: Boolean }) public new = false; + @property({ type: Boolean }) public disabled = false; + private _item?: Timer; @state() private _name!: string; @@ -77,6 +79,7 @@ class HaTimerForm extends LitElement { "ui.dialogs.helper_settings.required_error_msg" )} dialogInitialFocus + .disabled=${this.disabled} > @@ -130,6 +136,9 @@ class HaTimerForm extends LitElement { } private _toggleRestore() { + if (this.disabled) { + return; + } this._restore = !this._restore; fireEvent(this, "value-changed", { value: { ...this._item, restore: this._restore }, From 2eed4464920b80ae647afe1ec1ae22d0cedeb137 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 4 Nov 2025 11:10:18 +0100 Subject: [PATCH 26/66] Display entities without area in summary dashboard (#27777) * Add support for no area, no floor and no device in entity filter * Display entities without area in summary dashboard --- src/common/entity/entity_filter.ts | 41 +++++++----- .../strategies/climate-view-strategy.ts | 43 +++++++++++- .../light/strategies/light-view-strategy.ts | 40 ++++++++++- .../lovelace/cards/hui-home-summary-card.ts | 11 +--- .../home/home-media-players-view-strategy.ts | 47 ++++++++++++- .../safety/strategies/safety-view-strategy.ts | 43 +++++++++++- src/translations/en.json | 16 +++++ test/common/entity/entity_filter.test.ts | 66 +++++++++++++++++++ 8 files changed, 278 insertions(+), 29 deletions(-) diff --git a/src/common/entity/entity_filter.ts b/src/common/entity/entity_filter.ts index 7a8174f388..7382a5e229 100644 --- a/src/common/entity/entity_filter.ts +++ b/src/common/entity/entity_filter.ts @@ -9,9 +9,9 @@ type EntityCategory = "none" | "config" | "diagnostic"; export interface EntityFilter { domain?: string | string[]; device_class?: string | string[]; - device?: string | string[]; - area?: string | string[]; - floor?: string | string[]; + device?: string | null | (string | null)[]; + area?: string | null | (string | null)[]; + floor?: string | null | (string | null)[]; label?: string | string[]; entity_category?: EntityCategory | EntityCategory[]; hidden_platform?: string | string[]; @@ -19,6 +19,18 @@ export interface EntityFilter { export type EntityFilterFunc = (entityId: string) => boolean; +const normalizeFilterArray = ( + value: T | null | T[] | (T | null)[] | undefined +): Set | undefined => { + if (value === undefined) { + return undefined; + } + if (value === null) { + return new Set([null]); + } + return new Set(ensureArray(value)); +}; + export const generateEntityFilter = ( hass: HomeAssistant, filter: EntityFilter @@ -29,11 +41,9 @@ export const generateEntityFilter = ( const deviceClasses = filter.device_class ? new Set(ensureArray(filter.device_class)) : undefined; - const floors = filter.floor ? new Set(ensureArray(filter.floor)) : undefined; - const areas = filter.area ? new Set(ensureArray(filter.area)) : undefined; - const devices = filter.device - ? new Set(ensureArray(filter.device)) - : undefined; + const floors = normalizeFilterArray(filter.floor); + const areas = normalizeFilterArray(filter.area); + const devices = normalizeFilterArray(filter.device); const entityCategories = filter.entity_category ? new Set(ensureArray(filter.entity_category)) : undefined; @@ -73,23 +83,20 @@ export const generateEntityFilter = ( } if (floors) { - if (!floor || !floors.has(floor.floor_id)) { + const floorId = floor?.floor_id ?? null; + if (!floors.has(floorId)) { return false; } } if (areas) { - if (!area) { - return false; - } - if (!areas.has(area.area_id)) { + const areaId = area?.area_id ?? null; + if (!areas.has(areaId)) { return false; } } if (devices) { - if (!device) { - return false; - } - if (!devices.has(device.id)) { + const deviceId = device?.id ?? null; + if (!devices.has(deviceId)) { return false; } } diff --git a/src/panels/climate/strategies/climate-view-strategy.ts b/src/panels/climate/strategies/climate-view-strategy.ts index 0b22087419..26abc9a3bf 100644 --- a/src/panels/climate/strategies/climate-view-strategy.ts +++ b/src/panels/climate/strategies/climate-view-strategy.ts @@ -115,6 +115,24 @@ const processAreasForClimate = ( return cards; }; +const processUnassignedEntities = ( + hass: HomeAssistant, + entities: string[] +): LovelaceCardConfig[] => { + const unassignedFilter = generateEntityFilter(hass, { + area: null, + }); + const unassignedEntities = entities.filter(unassignedFilter); + const areaCards: LovelaceCardConfig[] = []; + const computeTileCard = computeAreaTileCardConfig(hass, "", true); + + for (const entityId of unassignedEntities) { + areaCards.push(computeTileCard(entityId)); + } + + return areaCards; +}; + @customElement("climate-view-strategy") export class ClimateViewStrategy extends ReactiveElement { static async generate( @@ -190,10 +208,33 @@ export class ClimateViewStrategy extends ReactiveElement { } } + // Process unassigned entities + const unassignedCards = processUnassignedEntities(hass, entities); + + if (unassignedCards.length > 0) { + const section: LovelaceSectionRawConfig = { + type: "grid", + column_span: 2, + cards: [ + { + type: "heading", + heading: + sections.length > 0 + ? hass.localize( + "ui.panel.lovelace.strategy.climate.other_devices" + ) + : hass.localize("ui.panel.lovelace.strategy.climate.devices"), + }, + ...unassignedCards, + ], + }; + sections.push(section); + } + return { type: "sections", max_columns: 2, - sections: sections || [], + sections: sections, }; } } diff --git a/src/panels/light/strategies/light-view-strategy.ts b/src/panels/light/strategies/light-view-strategy.ts index 9bed1dfbbf..b3de88f1cf 100644 --- a/src/panels/light/strategies/light-view-strategy.ts +++ b/src/panels/light/strategies/light-view-strategy.ts @@ -61,6 +61,24 @@ const processAreasForLight = ( return cards; }; +const processUnassignedLights = ( + hass: HomeAssistant, + entities: string[] +): LovelaceCardConfig[] => { + const unassignedFilter = generateEntityFilter(hass, { + area: null, + }); + const unassignedLights = entities.filter(unassignedFilter); + const areaCards: LovelaceCardConfig[] = []; + const computeTileCard = computeAreaTileCardConfig(hass, "", false); + + for (const entityId of unassignedLights) { + areaCards.push(computeTileCard(entityId)); + } + + return areaCards; +}; + @customElement("light-view-strategy") export class LightViewStrategy extends ReactiveElement { static async generate( @@ -136,10 +154,30 @@ export class LightViewStrategy extends ReactiveElement { } } + // Process unassigned lights + const unassignedCards = processUnassignedLights(hass, entities); + if (unassignedCards.length > 0) { + const section: LovelaceSectionRawConfig = { + type: "grid", + column_span: 2, + cards: [ + { + type: "heading", + heading: + sections.length > 0 + ? hass.localize("ui.panel.lovelace.strategy.light.other_lights") + : hass.localize("ui.panel.lovelace.strategy.light.lights"), + }, + ...unassignedCards, + ], + }; + sections.push(section); + } + return { type: "sections", max_columns: 2, - sections: sections || [], + sections: sections, }; } } diff --git a/src/panels/lovelace/cards/hui-home-summary-card.ts b/src/panels/lovelace/cards/hui-home-summary-card.ts index ff9f4d1e22..709ea77f3b 100644 --- a/src/panels/lovelace/cards/hui-home-summary-card.ts +++ b/src/panels/lovelace/cards/hui-home-summary-card.ts @@ -87,11 +87,6 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard { const allEntities = Object.keys(this.hass!.states); const areas = Object.values(this.hass.areas); - const areasFilter = generateEntityFilter(this.hass, { - area: areas.map((area) => area.area_id), - }); - - const entitiesInsideArea = allEntities.filter(areasFilter); switch (this._config.summary) { case "light": { @@ -100,7 +95,7 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard { generateEntityFilter(this.hass!, filter) ); - const lightEntities = findEntities(entitiesInsideArea, lightsFilters); + const lightEntities = findEntities(allEntities, lightsFilters); const onLights = lightEntities.filter((entityId) => { const s = this.hass!.states[entityId]?.state; @@ -153,7 +148,7 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard { generateEntityFilter(this.hass!, filter) ); - const safetyEntities = findEntities(entitiesInsideArea, safetyFilters); + const safetyEntities = findEntities(allEntities, safetyFilters); const locks = safetyEntities.filter((entityId) => { const domain = computeDomain(entityId); @@ -204,7 +199,7 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard { ); const mediaPlayerEntities = findEntities( - entitiesInsideArea, + allEntities, mediaPlayerFilters ); diff --git a/src/panels/lovelace/strategies/home/home-media-players-view-strategy.ts b/src/panels/lovelace/strategies/home/home-media-players-view-strategy.ts index 4a099166ec..04573e79bb 100644 --- a/src/panels/lovelace/strategies/home/home-media-players-view-strategy.ts +++ b/src/panels/lovelace/strategies/home/home-media-players-view-strategy.ts @@ -59,6 +59,26 @@ const processAreasForMediaPlayers = ( return cards; }; +const processUnassignedEntities = ( + hass: HomeAssistant, + entities: string[] +): LovelaceCardConfig[] => { + const unassignedFilter = generateEntityFilter(hass, { + area: null, + }); + const unassignedEntities = entities.filter(unassignedFilter); + const areaCards: LovelaceCardConfig[] = []; + + for (const entityId of unassignedEntities) { + areaCards.push({ + type: "media-control", + entity: entityId, + } satisfies MediaControlCardConfig); + } + + return areaCards; +}; + @customElement("home-media-players-view-strategy") export class HomeMMediaPlayersViewStrategy extends ReactiveElement { static async generate( @@ -134,10 +154,35 @@ export class HomeMMediaPlayersViewStrategy extends ReactiveElement { } } + // Process unassigned entities + const unassignedCards = processUnassignedEntities(hass, entities); + + if (unassignedCards.length > 0) { + const section: LovelaceSectionRawConfig = { + type: "grid", + column_span: 2, + cards: [ + { + type: "heading", + heading: + sections.length > 0 + ? hass.localize( + "ui.panel.lovelace.strategy.home_media_players.other_media_players" + ) + : hass.localize( + "ui.panel.lovelace.strategy.home_media_players.media_players" + ), + }, + ...unassignedCards, + ], + }; + sections.push(section); + } + return { type: "sections", max_columns: 2, - sections: sections || [], + sections: sections, }; } } diff --git a/src/panels/safety/strategies/safety-view-strategy.ts b/src/panels/safety/strategies/safety-view-strategy.ts index 5e04ab9d23..829c0c93ea 100644 --- a/src/panels/safety/strategies/safety-view-strategy.ts +++ b/src/panels/safety/strategies/safety-view-strategy.ts @@ -103,6 +103,24 @@ const processAreasForSafety = ( return cards; }; +const processUnassignedEntities = ( + hass: HomeAssistant, + entities: string[] +): LovelaceCardConfig[] => { + const unassignedFilter = generateEntityFilter(hass, { + area: null, + }); + const unassignedLights = entities.filter(unassignedFilter); + const areaCards: LovelaceCardConfig[] = []; + const computeTileCard = computeAreaTileCardConfig(hass, "", false); + + for (const entityId of unassignedLights) { + areaCards.push(computeTileCard(entityId)); + } + + return areaCards; +}; + @customElement("safety-view-strategy") export class SafetyViewStrategy extends ReactiveElement { static async generate( @@ -178,10 +196,33 @@ export class SafetyViewStrategy extends ReactiveElement { } } + // Process unassigned entities + const unassignedCards = processUnassignedEntities(hass, entities); + + if (unassignedCards.length > 0) { + const section: LovelaceSectionRawConfig = { + type: "grid", + column_span: 2, + cards: [ + { + type: "heading", + heading: + sections.length > 0 + ? hass.localize( + "ui.panel.lovelace.strategy.safety.other_devices" + ) + : hass.localize("ui.panel.lovelace.strategy.safety.devices"), + }, + ...unassignedCards, + ], + }; + sections.push(section); + } + return { type: "sections", max_columns: 2, - sections: sections || [], + sections: sections, }; } } diff --git a/src/translations/en.json b/src/translations/en.json index 293ebb81b7..192751fa99 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -6945,6 +6945,22 @@ "common_controls": { "not_loaded": "Usage Prediction integration is not loaded.", "no_data": "This place will soon fill up with the entities you use most often, based on your activity." + }, + "light": { + "lights": "Lights", + "other_lights": "Other lights" + }, + "safety": { + "devices": "Devices", + "other_devices": "Other devices" + }, + "climate": { + "devices": "Devices", + "other_devices": "Other devices" + }, + "home_media_players": { + "media_players": "Media players", + "other_media_players": "Other media players" } }, "cards": { diff --git a/test/common/entity/entity_filter.test.ts b/test/common/entity/entity_filter.test.ts index 032eadd32f..eba5346555 100644 --- a/test/common/entity/entity_filter.test.ts +++ b/test/common/entity/entity_filter.test.ts @@ -388,4 +388,70 @@ describe("generateEntityFilter", () => { expect(filter("light.no_area")).toBe(false); }); }); + + describe("null filtering", () => { + it("should filter entities with no area when null is used", () => { + const filter = generateEntityFilter(mockHass, { area: null }); + + expect(filter("light.no_area")).toBe(true); + expect(filter("light.living_room")).toBe(false); + }); + + it("should filter entities with specific area OR no area when null is in array", () => { + const filter = generateEntityFilter(mockHass, { + area: ["living_room", null], + }); + + expect(filter("light.living_room")).toBe(true); + expect(filter("sensor.temperature")).toBe(true); + expect(filter("light.no_area")).toBe(true); + expect(filter("switch.kitchen")).toBe(false); + }); + + it("should filter entities with no floor when null is used", () => { + const filter = generateEntityFilter(mockHass, { floor: null }); + + expect(filter("light.no_area")).toBe(true); + expect(filter("light.living_room")).toBe(false); + }); + + it("should filter entities with specific floor OR no floor", () => { + const filter = generateEntityFilter(mockHass, { + floor: ["main_floor", null], + }); + + expect(filter("light.living_room")).toBe(true); + expect(filter("switch.kitchen")).toBe(true); + expect(filter("light.no_area")).toBe(true); + expect(filter("light.bedroom")).toBe(false); + }); + + it("should filter entities with no device when null is used", () => { + const filter = generateEntityFilter(mockHass, { device: null }); + + expect(filter("light.living_room")).toBe(false); + expect(filter("light.no_area")).toBe(false); + }); + + it("should filter entities with specific device OR no device", () => { + const filter = generateEntityFilter(mockHass, { + device: ["device1", null], + }); + + expect(filter("light.living_room")).toBe(true); + expect(filter("switch.kitchen")).toBe(false); + }); + + it("should combine null filtering with other criteria", () => { + const filter = generateEntityFilter(mockHass, { + domain: "light", + area: ["living_room", null], + }); + + expect(filter("light.living_room")).toBe(true); + expect(filter("light.no_area")).toBe(true); + expect(filter("light.bedroom")).toBe(false); + expect(filter("sensor.temperature")).toBe(false); + }); + }); }); From 26c2369228b261ab8c8aa1c873bb60c2bdbcf520 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Mon, 3 Nov 2025 22:22:36 -0800 Subject: [PATCH 27/66] Fix sankey with external statistics devices (#27784) --- .../cards/energy/hui-energy-sankey-card.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/panels/lovelace/cards/energy/hui-energy-sankey-card.ts b/src/panels/lovelace/cards/energy/hui-energy-sankey-card.ts index 914c030dff..3b5f9f6e4c 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-sankey-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-sankey-card.ts @@ -419,13 +419,15 @@ class HuiEnergySankeyCard }; deviceNodes.forEach((deviceNode) => { const entity = this.hass.states[deviceNode.id]; - const { area, floor } = getEntityContext( - entity, - this.hass.entities, - this.hass.devices, - this.hass.areas, - this.hass.floors - ); + const { area, floor } = entity + ? getEntityContext( + entity, + this.hass.entities, + this.hass.devices, + this.hass.areas, + this.hass.floors + ) + : { area: null, floor: null }; if (area) { if (area.area_id in areas) { areas[area.area_id].value += deviceNode.value; From a6dfcb310044844228c838026a454bc3c9f3ef34 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 4 Nov 2025 09:45:26 +0100 Subject: [PATCH 28/66] Fix tooltip hide delay (#27786) --- src/components/ha-tooltip.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ha-tooltip.ts b/src/components/ha-tooltip.ts index 400e2e4397..6635da1361 100644 --- a/src/components/ha-tooltip.ts +++ b/src/components/ha-tooltip.ts @@ -9,7 +9,7 @@ export class HaTooltip extends Tooltip { @property({ attribute: "show-delay", type: Number }) showDelay = 150; /** The amount of time to wait before hiding the tooltip when the user mouses out.. */ - @property({ attribute: "hide-delay", type: Number }) hideDelay = 400; + @property({ attribute: "hide-delay", type: Number }) hideDelay = 150; static get styles(): CSSResultGroup { return [ From 15d67997e727c8c896e6d03cddebe0a91e8f1d14 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 4 Nov 2025 12:40:53 +0100 Subject: [PATCH 29/66] Don't show summary card if summary dashboards are empty (#27788) Don't show summary card if summary dashboard are empty --- .../home/home-main-view-strategy.ts | 169 ++++++++++-------- 1 file changed, 98 insertions(+), 71 deletions(-) diff --git a/src/panels/lovelace/strategies/home/home-main-view-strategy.ts b/src/panels/lovelace/strategies/home/home-main-view-strategy.ts index f0b3d32b88..caf2bde2b9 100644 --- a/src/panels/lovelace/strategies/home/home-main-view-strategy.ts +++ b/src/panels/lovelace/strategies/home/home-main-view-strategy.ts @@ -1,7 +1,11 @@ import { ReactiveElement } from "lit"; import { customElement } from "lit/decorators"; import { isComponentLoaded } from "../../../../common/config/is_component_loaded"; -import { generateEntityFilter } from "../../../../common/entity/entity_filter"; +import { + findEntities, + generateEntityFilter, +} from "../../../../common/entity/entity_filter"; +import { floorDefaultIcon } from "../../../../components/ha-floor-icon"; import type { AreaRegistryEntry } from "../../../../data/area_registry"; import { getEnergyPreferences } from "../../../../data/energy"; import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card"; @@ -21,7 +25,7 @@ import type { import { getAreas, getFloors } from "../areas/helpers/areas-strategy-helper"; import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy"; import { getHomeStructure } from "./helpers/home-structure"; -import { floorDefaultIcon } from "../../../../components/ha-floor-icon"; +import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries"; export interface HomeMainViewStrategyConfig { type: "home-main"; @@ -144,82 +148,105 @@ export class HomeMainViewStrategy extends ReactiveElement { column_span: maxColumns, } as LovelaceStrategySectionConfig; - // Check if there are any media player entities - const mediaPlayerFilter = generateEntityFilter(hass, { - domain: "media_player", - entity_category: "none", - }); - const hasMediaPlayers = Object.keys(hass.states).some(mediaPlayerFilter); + const allEntities = Object.keys(hass.states); + + const mediaPlayerFilter = HOME_SUMMARIES_FILTERS.media_players.map( + (filter) => generateEntityFilter(hass, filter) + ); + + const lightsFilters = HOME_SUMMARIES_FILTERS.light.map((filter) => + generateEntityFilter(hass, filter) + ); + + const climateFilters = HOME_SUMMARIES_FILTERS.climate.map((filter) => + generateEntityFilter(hass, filter) + ); + + const safetyFilters = HOME_SUMMARIES_FILTERS.safety.map((filter) => + generateEntityFilter(hass, filter) + ); + + const hasLights = findEntities(allEntities, lightsFilters).length > 0; + const hasMediaPlayers = + findEntities(allEntities, mediaPlayerFilter).length > 0; + const hasClimate = findEntities(allEntities, climateFilters).length > 0; + const hasSafety = findEntities(allEntities, safetyFilters).length > 0; const summaryCards: LovelaceCardConfig[] = [ - { - type: "heading", - heading: hass.localize("ui.panel.lovelace.strategy.home.summaries"), - }, - { - type: "home-summary", - summary: "light", - vertical: true, - tap_action: { - action: "navigate", - navigation_path: "/light?historyBack=1", - }, - grid_options: { - rows: 2, - columns: 4, - }, - } satisfies HomeSummaryCard, - { - type: "home-summary", - summary: "climate", - vertical: true, - tap_action: { - action: "navigate", - navigation_path: "/climate?historyBack=1", - }, - grid_options: { - rows: 2, - columns: 4, - }, - } satisfies HomeSummaryCard, - { - type: "home-summary", - summary: "safety", - vertical: true, - tap_action: { - action: "navigate", - navigation_path: "/safety?historyBack=1", - }, - grid_options: { - rows: 2, - columns: 4, - }, - } satisfies HomeSummaryCard, - ]; - - // Only add media players summary if there are media player entities - if (hasMediaPlayers) { - summaryCards.push({ - type: "home-summary", - summary: "media_players", - vertical: true, - tap_action: { - action: "navigate", - navigation_path: "media-players", - }, - grid_options: { - rows: 2, - columns: 4, - }, - } satisfies HomeSummaryCard); - } + hasLights && + ({ + type: "home-summary", + summary: "light", + vertical: true, + tap_action: { + action: "navigate", + navigation_path: "/light?historyBack=1", + }, + grid_options: { + rows: 2, + columns: 4, + }, + } satisfies HomeSummaryCard), + hasClimate && + ({ + type: "home-summary", + summary: "climate", + vertical: true, + tap_action: { + action: "navigate", + navigation_path: "/climate?historyBack=1", + }, + grid_options: { + rows: 2, + columns: 4, + }, + } satisfies HomeSummaryCard), + hasSafety && + ({ + type: "home-summary", + summary: "safety", + vertical: true, + tap_action: { + action: "navigate", + navigation_path: "/safety?historyBack=1", + }, + grid_options: { + rows: 2, + columns: 4, + }, + } satisfies HomeSummaryCard), + hasMediaPlayers && + ({ + type: "home-summary", + summary: "media_players", + vertical: true, + tap_action: { + action: "navigate", + navigation_path: "media-players", + }, + grid_options: { + rows: 2, + columns: 4, + }, + } satisfies HomeSummaryCard), + ].filter(Boolean) as LovelaceCardConfig[]; const summarySection: LovelaceSectionConfig = { type: "grid", column_span: maxColumns, - cards: summaryCards, + cards: [], }; + if (summaryCards.length) { + summarySection.cards!.push( + { + type: "heading", + heading: hass.localize("ui.panel.lovelace.strategy.home.summaries"), + }, + ...summaryCards + ); + } + const weatherFilter = generateEntityFilter(hass, { domain: "weather", entity_category: "none", @@ -275,7 +302,7 @@ export class HomeMainViewStrategy extends ReactiveElement { [ favoriteSection.cards && favoriteSection, commonControlsSection, - summarySection, + summarySection.cards && summarySection, ...floorsSections, widgetSection.cards && widgetSection, ] satisfies (LovelaceSectionRawConfig | undefined)[] From 3c82d12609db003b518f0a998c80ebe83670092a Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 4 Nov 2025 17:41:27 +0100 Subject: [PATCH 30/66] Auto refresh summary dashboard when registries changed (#27794) --- src/panels/climate/ha-panel-climate.ts | 137 +++++++++++++++++-------- src/panels/light/ha-panel-light.ts | 133 +++++++++++++++++------- src/panels/safety/ha-panel-safety.ts | 135 +++++++++++++++++------- 3 files changed, 286 insertions(+), 119 deletions(-) diff --git a/src/panels/climate/ha-panel-climate.ts b/src/panels/climate/ha-panel-climate.ts index 3c5a5b8ebb..0540da1ebd 100644 --- a/src/panels/climate/ha-panel-climate.ts +++ b/src/panels/climate/ha-panel-climate.ts @@ -1,24 +1,23 @@ -import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; -import { LitElement, css, html } from "lit"; +import type { CSSResultGroup, PropertyValues } from "lit"; +import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { goBack } from "../../common/navigate"; +import { debounce } from "../../common/util/debounce"; +import { deepEqual } from "../../common/util/deep-equal"; import "../../components/ha-icon-button-arrow-prev"; import "../../components/ha-menu-button"; -import type { LovelaceConfig } from "../../data/lovelace/config/types"; +import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view"; import { haStyle } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; +import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy"; import type { Lovelace } from "../lovelace/types"; import "../lovelace/views/hui-view"; import "../lovelace/views/hui-view-container"; -const CLIMATE_LOVELACE_CONFIG: LovelaceConfig = { - views: [ - { - strategy: { - type: "climate", - }, - }, - ], +const CLIMATE_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = { + strategy: { + type: "climate", + }, }; @customElement("ha-panel-climate") @@ -33,65 +32,119 @@ class PanelClimate extends LitElement { @state() private _searchParms = new URLSearchParams(window.location.search); - public firstUpdated(_changedProperties: PropertyValues): void { - super.firstUpdated(_changedProperties); - } - public willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); + // Initial setup if (!this.hasUpdated) { this.hass.loadFragmentTranslation("lovelace"); + this._setLovelace(); + return; } + if (!changedProps.has("hass")) { return; } + const oldHass = changedProps.get("hass") as this["hass"]; - if (oldHass?.locale !== this.hass.locale) { + if (oldHass && oldHass.localize !== this.hass.localize) { this._setLovelace(); + return; + } + + if (oldHass && this.hass) { + // If the entity registry changed, ask the user if they want to refresh the config + if ( + oldHass.entities !== this.hass.entities || + oldHass.devices !== this.hass.devices || + oldHass.areas !== this.hass.areas || + oldHass.floors !== this.hass.floors + ) { + if (this.hass.config.state === "RUNNING") { + this._debounceRegistriesChanged(); + return; + } + } + // If ha started, refresh the config + if ( + this.hass.config.state === "RUNNING" && + oldHass.config.state !== "RUNNING" + ) { + this._setLovelace(); + } } } + private _debounceRegistriesChanged = debounce( + () => this._registriesChanged(), + 200 + ); + + private _registriesChanged = async () => { + this._setLovelace(); + }; + private _back(ev) { ev.stopPropagation(); goBack(); } - protected render(): TemplateResult { + protected render() { return html`
- ${this._searchParms.has("historyBack") - ? html` - - ` - : html` - - `} + ${ + this._searchParms.has("historyBack") + ? html` + + ` + : html` + + ` + }
${this.hass.localize("panel.climate")}
- - - + ${ + this._lovelace + ? html` + + + ` + : nothing + } `; } - private _setLovelace() { + private async _setLovelace() { + const viewConfig = await generateLovelaceViewStrategy( + CLIMATE_LOVELACE_VIEW_CONFIG, + this.hass + ); + + const config = { views: [viewConfig] }; + const rawConfig = { views: [CLIMATE_LOVELACE_VIEW_CONFIG] }; + + if (deepEqual(config, this._lovelace?.config)) { + return; + } + this._lovelace = { - config: CLIMATE_LOVELACE_CONFIG, - rawConfig: CLIMATE_LOVELACE_CONFIG, + config: config, + rawConfig: rawConfig, editMode: false, urlPath: "climate", mode: "generated", diff --git a/src/panels/light/ha-panel-light.ts b/src/panels/light/ha-panel-light.ts index d2df36e7a5..a2eb0cc683 100644 --- a/src/panels/light/ha-panel-light.ts +++ b/src/panels/light/ha-panel-light.ts @@ -1,24 +1,23 @@ -import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; -import { LitElement, css, html } from "lit"; +import type { CSSResultGroup, PropertyValues } from "lit"; +import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { goBack } from "../../common/navigate"; +import { debounce } from "../../common/util/debounce"; +import { deepEqual } from "../../common/util/deep-equal"; import "../../components/ha-icon-button-arrow-prev"; import "../../components/ha-menu-button"; -import type { LovelaceConfig } from "../../data/lovelace/config/types"; +import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view"; import { haStyle } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; +import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy"; import type { Lovelace } from "../lovelace/types"; import "../lovelace/views/hui-view"; import "../lovelace/views/hui-view-container"; -const LIGHT_LOVELACE_CONFIG: LovelaceConfig = { - views: [ - { - strategy: { - type: "light", - }, - }, - ], +const LIGHT_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = { + strategy: { + type: "light", + }, }; @customElement("ha-panel-light") @@ -34,60 +33,118 @@ class PanelLight extends LitElement { @state() private _searchParms = new URLSearchParams(window.location.search); public willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); + // Initial setup if (!this.hasUpdated) { this.hass.loadFragmentTranslation("lovelace"); + this._setLovelace(); + return; } + if (!changedProps.has("hass")) { return; } + const oldHass = changedProps.get("hass") as this["hass"]; - if (oldHass?.locale !== this.hass.locale) { + if (oldHass && oldHass.localize !== this.hass.localize) { this._setLovelace(); + return; + } + + if (oldHass && this.hass) { + // If the entity registry changed, ask the user if they want to refresh the config + if ( + oldHass.entities !== this.hass.entities || + oldHass.devices !== this.hass.devices || + oldHass.areas !== this.hass.areas || + oldHass.floors !== this.hass.floors + ) { + if (this.hass.config.state === "RUNNING") { + this._debounceRegistriesChanged(); + return; + } + } + // If ha started, refresh the config + if ( + this.hass.config.state === "RUNNING" && + oldHass.config.state !== "RUNNING" + ) { + this._setLovelace(); + } } } + private _debounceRegistriesChanged = debounce( + () => this._registriesChanged(), + 200 + ); + + private _registriesChanged = async () => { + this._setLovelace(); + }; + private _back(ev) { ev.stopPropagation(); goBack(); } - protected render(): TemplateResult { + protected render() { return html`
- ${this._searchParms.has("historyBack") - ? html` - - ` - : html` - - `} + ${ + this._searchParms.has("historyBack") + ? html` + + ` + : html` + + ` + }
${this.hass.localize("panel.light")}
- - - + ${ + this._lovelace + ? html` + + + ` + : nothing + } `; } - private _setLovelace() { + private async _setLovelace() { + const viewConfig = await generateLovelaceViewStrategy( + LIGHT_LOVELACE_VIEW_CONFIG, + this.hass + ); + + const config = { views: [viewConfig] }; + const rawConfig = { views: [LIGHT_LOVELACE_VIEW_CONFIG] }; + + if (deepEqual(config, this._lovelace?.config)) { + return; + } + this._lovelace = { - config: LIGHT_LOVELACE_CONFIG, - rawConfig: LIGHT_LOVELACE_CONFIG, + config: config, + rawConfig: rawConfig, editMode: false, urlPath: "light", mode: "generated", diff --git a/src/panels/safety/ha-panel-safety.ts b/src/panels/safety/ha-panel-safety.ts index af6757232a..00bf20071b 100644 --- a/src/panels/safety/ha-panel-safety.ts +++ b/src/panels/safety/ha-panel-safety.ts @@ -1,24 +1,23 @@ -import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; -import { LitElement, css, html } from "lit"; +import type { CSSResultGroup, PropertyValues } from "lit"; +import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { goBack } from "../../common/navigate"; +import { debounce } from "../../common/util/debounce"; +import { deepEqual } from "../../common/util/deep-equal"; import "../../components/ha-icon-button-arrow-prev"; import "../../components/ha-menu-button"; -import type { LovelaceConfig } from "../../data/lovelace/config/types"; +import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view"; import { haStyle } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; +import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy"; import type { Lovelace } from "../lovelace/types"; import "../lovelace/views/hui-view"; import "../lovelace/views/hui-view-container"; -const SAFETY_LOVELACE_CONFIG: LovelaceConfig = { - views: [ - { - strategy: { - type: "safety", - }, - }, - ], +const SECURITY_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = { + strategy: { + type: "security", + }, }; @customElement("ha-panel-safety") @@ -34,60 +33,118 @@ class PanelSafety extends LitElement { @state() private _searchParms = new URLSearchParams(window.location.search); public willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); + // Initial setup if (!this.hasUpdated) { this.hass.loadFragmentTranslation("lovelace"); + this._setLovelace(); + return; } + if (!changedProps.has("hass")) { return; } + const oldHass = changedProps.get("hass") as this["hass"]; - if (oldHass?.locale !== this.hass.locale) { + if (oldHass && oldHass.localize !== this.hass.localize) { this._setLovelace(); + return; + } + + if (oldHass && this.hass) { + // If the entity registry changed, ask the user if they want to refresh the config + if ( + oldHass.entities !== this.hass.entities || + oldHass.devices !== this.hass.devices || + oldHass.areas !== this.hass.areas || + oldHass.floors !== this.hass.floors + ) { + if (this.hass.config.state === "RUNNING") { + this._debounceRegistriesChanged(); + return; + } + } + // If ha started, refresh the config + if ( + this.hass.config.state === "RUNNING" && + oldHass.config.state !== "RUNNING" + ) { + this._setLovelace(); + } } } + private _debounceRegistriesChanged = debounce( + () => this._registriesChanged(), + 200 + ); + + private _registriesChanged = async () => { + this._setLovelace(); + }; + private _back(ev) { ev.stopPropagation(); goBack(); } - protected render(): TemplateResult { + protected render() { return html`
- ${this._searchParms.has("historyBack") - ? html` - - ` - : html` - - `} -
${this.hass.localize("panel.safety")}
+ ${ + this._searchParms.has("historyBack") + ? html` + + ` + : html` + + ` + } +
${this.hass.localize("panel.security")}
- - - + ${ + this._lovelace + ? html` + + + ` + : nothing + } `; } - private _setLovelace() { + private async _setLovelace() { + const viewConfig = await generateLovelaceViewStrategy( + SECURITY_LOVELACE_VIEW_CONFIG, + this.hass + ); + + const config = { views: [viewConfig] }; + const rawConfig = { views: [SECURITY_LOVELACE_VIEW_CONFIG] }; + + if (deepEqual(config, this._lovelace?.config)) { + return; + } + this._lovelace = { - config: SAFETY_LOVELACE_CONFIG, - rawConfig: SAFETY_LOVELACE_CONFIG, + config: config, + rawConfig: rawConfig, editMode: false, urlPath: "safety", mode: "generated", From d23e45e410173cbdf6ebc69b61a9327f74653f0f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 4 Nov 2025 18:02:56 +0100 Subject: [PATCH 31/66] Handle unknown items in target picker (#27795) * Handle unknown items in target picker * Update ha-target-picker-item-row.ts * update colors * fallback to domain icons --- .../ha-target-picker-item-row.ts | 82 +++++++++++++------ src/translations/en.json | 5 ++ 2 files changed, 61 insertions(+), 26 deletions(-) diff --git a/src/components/target-picker/ha-target-picker-item-row.ts b/src/components/target-picker/ha-target-picker-item-row.ts index 4884d82902..f7a5db23c8 100644 --- a/src/components/target-picker/ha-target-picker-item-row.ts +++ b/src/components/target-picker/ha-target-picker-item-row.ts @@ -6,6 +6,7 @@ import { mdiLabel, mdiTextureBox, } from "@mdi/js"; +import type { HassEntity } from "home-assistant-js-websocket"; import { css, html, LitElement, nothing, type PropertyValues } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; @@ -19,9 +20,12 @@ 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 { computeRTL } from "../../common/util/compute_rtl"; +import type { AreaRegistryEntry } from "../../data/area_registry"; import { getConfigEntry } from "../../data/config_entries"; import { labelsContext } from "../../data/context"; +import type { DeviceRegistryEntry } from "../../data/device_registry"; import type { HaEntityPickerEntityFilterFunc } from "../../data/entity"; +import type { FloorRegistryEntry } from "../../data/floor_registry"; import { domainToName } from "../../data/integration"; import type { LabelRegistryEntry } from "../../data/label_registry"; import { @@ -111,10 +115,10 @@ export class HaTargetPickerItemRow extends LitElement { } protected render() { - const { name, context, iconPath, fallbackIconPath, stateObject } = + const { name, context, iconPath, fallbackIconPath, stateObject, notFound } = this._itemData(this.type, this.itemId); - const showEntities = this.type !== "entity"; + const showEntities = this.type !== "entity" && !notFound; const entries = this.parentEntries || this._entries; @@ -128,7 +132,7 @@ export class HaTargetPickerItemRow extends LitElement { } return html` - +
${this.subEntry ? html` @@ -148,11 +152,15 @@ export class HaTargetPickerItemRow extends LitElement { />` : fallbackIconPath ? html`` - : stateObject + : this.type === "entity" ? html` ` @@ -160,8 +168,14 @@ export class HaTargetPickerItemRow extends LitElement {
${name}
- ${context && !this.hideContext - ? html`${context}` + ${notFound || (context && !this.hideContext) + ? html`${notFound + ? this.hass.localize( + `ui.components.target-picker.${this.type}_not_found` + ) + : context}` : nothing} ${this._domainName && this.subEntry ? html` { if (type === "floor") { - const floor = this.hass.floors?.[item]; + const floor: FloorRegistryEntry | undefined = this.hass.floors?.[item]; return { name: floor?.name || item, iconPath: floor?.icon, fallbackIconPath: floor ? floorDefaultIconPath(floor) : mdiHome, + notFound: !floor, }; } if (type === "area") { - const area = this.hass.areas?.[item]; + const area: AreaRegistryEntry | undefined = this.hass.areas?.[item]; return { name: area?.name || item, - context: area.floor_id && this.hass.floors?.[area.floor_id]?.name, + context: area?.floor_id && this.hass.floors?.[area.floor_id]?.name, iconPath: area?.icon, fallbackIconPath: mdiTextureBox, + notFound: !area, }; } if (type === "device") { - const device = this.hass.devices?.[item]; + const device: DeviceRegistryEntry | undefined = this.hass.devices?.[item]; - if (device.primary_config_entry) { + if (device?.primary_config_entry) { this._getDeviceDomain(device.primary_config_entry); } @@ -501,24 +517,25 @@ export class HaTargetPickerItemRow extends LitElement { name: device ? computeDeviceNameDisplay(device, this.hass) : item, context: device?.area_id && this.hass.areas?.[device.area_id]?.name, fallbackIconPath: mdiDevices, + notFound: !device, }; } if (type === "entity") { this._setDomainName(computeDomain(item)); - const stateObject = this.hass.states[item]; - const entityName = computeEntityName( - stateObject, - this.hass.entities, - this.hass.devices - ); - const { area, device } = getEntityContext( - stateObject, - this.hass.entities, - this.hass.devices, - this.hass.areas, - this.hass.floors - ); + const stateObject: HassEntity | undefined = this.hass.states[item]; + const entityName = stateObject + ? computeEntityName(stateObject, this.hass.entities, this.hass.devices) + : item; + const { area, device } = stateObject + ? getEntityContext( + stateObject, + this.hass.entities, + this.hass.devices, + this.hass.areas, + this.hass.floors + ) + : { area: undefined, device: undefined }; const deviceName = device ? computeDeviceName(device) : undefined; const areaName = area ? computeAreaName(area) : undefined; const context = [areaName, entityName ? deviceName : undefined] @@ -528,15 +545,19 @@ export class HaTargetPickerItemRow extends LitElement { name: entityName || deviceName || item, context, stateObject, + notFound: !stateObject, }; } // type label - const label = this._labelRegistry.find((lab) => lab.label_id === item); + const label: LabelRegistryEntry | undefined = this._labelRegistry.find( + (lab) => lab.label_id === item + ); return { name: label?.name || item, iconPath: label?.icon, fallbackIconPath: mdiLabel, + notFound: !label, }; }); @@ -597,11 +618,20 @@ export class HaTargetPickerItemRow extends LitElement { border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg)); } + .error { + background: var(--ha-color-fill-warning-quiet-resting); + } + + .error [slot="supporting-text"] { + color: var(--ha-color-on-warning-normal); + } + state-badge { color: var(--ha-color-on-neutral-quiet); } .icon { + width: 24px; display: flex; } diff --git a/src/translations/en.json b/src/translations/en.json index 192751fa99..6ae6ff6cde 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -695,6 +695,11 @@ "remove_device_id": "Remove device", "remove_entity_id": "Remove entity", "remove_label_id": "Remove label", + "floor_not_found": "Floor not found", + "area_not_found": "Area not found", + "device_not_found": "Device not found", + "entity_not_found": "Entity not found", + "label_not_found": "Label not found", "devices_count": "{count} {count, plural,\n one {device}\n other {devices}\n}", "entities_count": "{count} {count, plural,\n one {entity}\n other {entities}\n}", "target_details": "Target details", From eec99b2fa323bc17da65e701b428e48f494aa2e6 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 4 Nov 2025 17:23:06 +0100 Subject: [PATCH 32/66] Rename safety panel to security panel (#27796) --- src/layouts/partial-panel-resolver.ts | 2 +- .../ha-config-lovelace-dashboards.ts | 10 +++--- .../lovelace/cards/hui-home-summary-card.ts | 12 +++---- .../lovelace/strategies/get-strategy.ts | 2 +- .../strategies/home/helpers/home-summaries.ts | 10 +++--- .../home/home-area-view-strategy.ts | 12 +++---- .../home/home-main-view-strategy.ts | 10 +++--- .../ha-panel-security.ts} | 8 ++--- .../strategies/security-view-strategy.ts} | 32 +++++++++---------- src/translations/en.json | 4 +-- 10 files changed, 51 insertions(+), 51 deletions(-) rename src/panels/{safety/ha-panel-safety.ts => security/ha-panel-security.ts} (98%) rename src/panels/{safety/strategies/safety-view-strategy.ts => security/strategies/security-view-strategy.ts} (85%) diff --git a/src/layouts/partial-panel-resolver.ts b/src/layouts/partial-panel-resolver.ts index fd5d0ecc37..233ace7fb1 100644 --- a/src/layouts/partial-panel-resolver.ts +++ b/src/layouts/partial-panel-resolver.ts @@ -33,7 +33,7 @@ const COMPONENTS = { "media-browser": () => import("../panels/media-browser/ha-panel-media-browser"), light: () => import("../panels/light/ha-panel-light"), - safety: () => import("../panels/safety/ha-panel-safety"), + security: () => import("../panels/security/ha-panel-security"), climate: () => import("../panels/climate/ha-panel-climate"), }; 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 c2ebbb0640..59664bdf5d 100644 --- a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts +++ b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts @@ -332,13 +332,13 @@ export class HaConfigLovelaceDashboards extends LitElement { }); } - if (this.hass.panels.safety) { + if (this.hass.panels.security) { result.push({ icon: "mdi:security", - title: this.hass.localize("panel.safety"), + title: this.hass.localize("panel.security"), show_in_sidebar: false, mode: "storage", - url_path: "safety", + url_path: "security", filename: "", default: false, require_admin: false, @@ -470,13 +470,13 @@ export class HaConfigLovelaceDashboards extends LitElement { } private _canDelete(urlPath: string) { - return !["lovelace", "energy", "light", "safety", "climate"].includes( + return !["lovelace", "energy", "light", "security", "climate"].includes( urlPath ); } private _canEdit(urlPath: string) { - return !["light", "safety", "climate"].includes(urlPath); + return !["light", "security", "climate"].includes(urlPath); } private _handleDelete = async (item: DataTableItem) => { diff --git a/src/panels/lovelace/cards/hui-home-summary-card.ts b/src/panels/lovelace/cards/hui-home-summary-card.ts index 709ea77f3b..e7981dccbb 100644 --- a/src/panels/lovelace/cards/hui-home-summary-card.ts +++ b/src/panels/lovelace/cards/hui-home-summary-card.ts @@ -33,7 +33,7 @@ import type { HomeSummaryCard } from "./types"; const COLORS: Record = { light: "amber", climate: "deep-orange", - safety: "blue-grey", + security: "blue-grey", media_players: "blue", }; @@ -142,20 +142,20 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard { ? `${formattedMinTemp}°` : `${formattedMinTemp} - ${formattedMaxTemp}°`; } - case "safety": { + case "security": { // Alarm and lock status - const safetyFilters = HOME_SUMMARIES_FILTERS.safety.map((filter) => + const securityFilters = HOME_SUMMARIES_FILTERS.security.map((filter) => generateEntityFilter(this.hass!, filter) ); - const safetyEntities = findEntities(allEntities, safetyFilters); + const securityEntities = findEntities(allEntities, securityFilters); - const locks = safetyEntities.filter((entityId) => { + const locks = securityEntities.filter((entityId) => { const domain = computeDomain(entityId); return domain === "lock"; }); - const alarms = safetyEntities.filter((entityId) => { + const alarms = securityEntities.filter((entityId) => { const domain = computeDomain(entityId); return domain === "alarm_control_panel"; }); diff --git a/src/panels/lovelace/strategies/get-strategy.ts b/src/panels/lovelace/strategies/get-strategy.ts index 80a4cb3ed8..cf2615bf0a 100644 --- a/src/panels/lovelace/strategies/get-strategy.ts +++ b/src/panels/lovelace/strategies/get-strategy.ts @@ -48,7 +48,7 @@ const STRATEGIES: Record> = { import("./home/home-media-players-view-strategy"), "home-area": () => import("./home/home-area-view-strategy"), light: () => import("../../light/strategies/light-view-strategy"), - safety: () => import("../../safety/strategies/safety-view-strategy"), + security: () => import("../../security/strategies/security-view-strategy"), climate: () => import("../../climate/strategies/climate-view-strategy"), }, section: { diff --git a/src/panels/lovelace/strategies/home/helpers/home-summaries.ts b/src/panels/lovelace/strategies/home/helpers/home-summaries.ts index 27d6dcb2b6..5f3c9557a1 100644 --- a/src/panels/lovelace/strategies/home/helpers/home-summaries.ts +++ b/src/panels/lovelace/strategies/home/helpers/home-summaries.ts @@ -2,12 +2,12 @@ import type { EntityFilter } from "../../../../../common/entity/entity_filter"; import type { LocalizeFunc } from "../../../../../common/translations/localize"; import { climateEntityFilters } from "../../../../climate/strategies/climate-view-strategy"; import { lightEntityFilters } from "../../../../light/strategies/light-view-strategy"; -import { safetyEntityFilters } from "../../../../safety/strategies/safety-view-strategy"; +import { securityEntityFilters } from "../../../../security/strategies/security-view-strategy"; export const HOME_SUMMARIES = [ "light", "climate", - "safety", + "security", "media_players", ] as const; @@ -16,14 +16,14 @@ export type HomeSummary = (typeof HOME_SUMMARIES)[number]; export const HOME_SUMMARIES_ICONS: Record = { light: "mdi:lamps", climate: "mdi:home-thermometer", - safety: "mdi:security", + security: "mdi:security", media_players: "mdi:multimedia", }; export const HOME_SUMMARIES_FILTERS: Record = { light: lightEntityFilters, climate: climateEntityFilters, - safety: safetyEntityFilters, + security: securityEntityFilters, media_players: [{ domain: "media_player", entity_category: "none" }], }; @@ -31,7 +31,7 @@ export const getSummaryLabel = ( localize: LocalizeFunc, summary: HomeSummary ) => { - if (summary === "light" || summary === "climate" || summary === "safety") { + if (summary === "light" || summary === "climate" || summary === "security") { return localize(`panel.${summary}`); } return localize(`ui.panel.lovelace.strategy.home.summary_list.${summary}`); diff --git a/src/panels/lovelace/strategies/home/home-area-view-strategy.ts b/src/panels/lovelace/strategies/home/home-area-view-strategy.ts index 1c8a5372b4..414fef4c62 100644 --- a/src/panels/lovelace/strategies/home/home-area-view-strategy.ts +++ b/src/panels/lovelace/strategies/home/home-area-view-strategy.ts @@ -104,7 +104,7 @@ export class HomeAreaViewStrategy extends ReactiveElement { const { light, climate, - safety, + security, media_players: mediaPlayers, } = entitiesBySummary; @@ -136,16 +136,16 @@ export class HomeAreaViewStrategy extends ReactiveElement { }); } - if (safety.length > 0) { + if (security.length > 0) { sections.push({ type: "grid", cards: [ computeHeadingCard( - getSummaryLabel(hass.localize, "safety"), - HOME_SUMMARIES_ICONS.safety, - "/safety?historyBack=1" + getSummaryLabel(hass.localize, "security"), + HOME_SUMMARIES_ICONS.security, + "/security?historyBack=1" ), - ...safety.map(computeTileCard), + ...security.map(computeTileCard), ], }); } diff --git a/src/panels/lovelace/strategies/home/home-main-view-strategy.ts b/src/panels/lovelace/strategies/home/home-main-view-strategy.ts index caf2bde2b9..23acca02c5 100644 --- a/src/panels/lovelace/strategies/home/home-main-view-strategy.ts +++ b/src/panels/lovelace/strategies/home/home-main-view-strategy.ts @@ -162,7 +162,7 @@ export class HomeMainViewStrategy extends ReactiveElement { generateEntityFilter(hass, filter) ); - const safetyFilters = HOME_SUMMARIES_FILTERS.safety.map((filter) => + const securityFilters = HOME_SUMMARIES_FILTERS.security.map((filter) => generateEntityFilter(hass, filter) ); @@ -170,7 +170,7 @@ export class HomeMainViewStrategy extends ReactiveElement { const hasMediaPlayers = findEntities(allEntities, mediaPlayerFilter).length > 0; const hasClimate = findEntities(allEntities, climateFilters).length > 0; - const hasSafety = findEntities(allEntities, safetyFilters).length > 0; + const hasSecurity = findEntities(allEntities, securityFilters).length > 0; const summaryCards: LovelaceCardConfig[] = [ hasLights && @@ -201,14 +201,14 @@ export class HomeMainViewStrategy extends ReactiveElement { columns: 4, }, } satisfies HomeSummaryCard), - hasSafety && + hasSecurity && ({ type: "home-summary", - summary: "safety", + summary: "security", vertical: true, tap_action: { action: "navigate", - navigation_path: "/safety?historyBack=1", + navigation_path: "/security?historyBack=1", }, grid_options: { rows: 2, diff --git a/src/panels/safety/ha-panel-safety.ts b/src/panels/security/ha-panel-security.ts similarity index 98% rename from src/panels/safety/ha-panel-safety.ts rename to src/panels/security/ha-panel-security.ts index 00bf20071b..154104d0ea 100644 --- a/src/panels/safety/ha-panel-safety.ts +++ b/src/panels/security/ha-panel-security.ts @@ -20,8 +20,8 @@ const SECURITY_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = { }, }; -@customElement("ha-panel-safety") -class PanelSafety extends LitElement { +@customElement("ha-panel-security") +class PanelSecurity extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ type: Boolean, reflect: true }) public narrow = false; @@ -146,7 +146,7 @@ class PanelSafety extends LitElement { config: config, rawConfig: rawConfig, editMode: false, - urlPath: "safety", + urlPath: "security", mode: "generated", locale: this.hass.locale, enableFullEditMode: () => undefined, @@ -248,6 +248,6 @@ class PanelSafety extends LitElement { declare global { interface HTMLElementTagNameMap { - "ha-panel-safety": PanelSafety; + "ha-panel-security": PanelSecurity; } } diff --git a/src/panels/safety/strategies/safety-view-strategy.ts b/src/panels/security/strategies/security-view-strategy.ts similarity index 85% rename from src/panels/safety/strategies/safety-view-strategy.ts rename to src/panels/security/strategies/security-view-strategy.ts index 829c0c93ea..18cb22b184 100644 --- a/src/panels/safety/strategies/safety-view-strategy.ts +++ b/src/panels/security/strategies/security-view-strategy.ts @@ -17,11 +17,11 @@ import { } from "../../lovelace/strategies/areas/helpers/areas-strategy-helper"; import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure"; -export interface SafetyViewStrategyConfig { - type: "safety"; +export interface SecurityViewStrategyConfig { + type: "security"; } -export const safetyEntityFilters: EntityFilter[] = [ +export const securityEntityFilters: EntityFilter[] = [ { domain: "camera", entity_category: "none", @@ -67,7 +67,7 @@ export const safetyEntityFilters: EntityFilter[] = [ }, ]; -const processAreasForSafety = ( +const processAreasForSecurity = ( areaIds: string[], hass: HomeAssistant, entities: string[] @@ -81,12 +81,12 @@ const processAreasForSafety = ( const areaFilter = generateEntityFilter(hass, { area: area.area_id, }); - const areaSafetyEntities = entities.filter(areaFilter); + const areaSecurityEntities = entities.filter(areaFilter); const areaCards: LovelaceCardConfig[] = []; const computeTileCard = computeAreaTileCardConfig(hass, "", false); - for (const entityId of areaSafetyEntities) { + for (const entityId of areaSecurityEntities) { areaCards.push(computeTileCard(entityId)); } @@ -121,10 +121,10 @@ const processUnassignedEntities = ( return areaCards; }; -@customElement("safety-view-strategy") -export class SafetyViewStrategy extends ReactiveElement { +@customElement("security-view-strategy") +export class SecurityViewStrategy extends ReactiveElement { static async generate( - _config: SafetyViewStrategyConfig, + _config: SecurityViewStrategyConfig, hass: HomeAssistant ): Promise { const areas = getAreas(hass.areas); @@ -135,11 +135,11 @@ export class SafetyViewStrategy extends ReactiveElement { const allEntities = Object.keys(hass.states); - const safetyFilters = safetyEntityFilters.map((filter) => + const securityFilters = securityEntityFilters.map((filter) => generateEntityFilter(hass, filter) ); - const entities = findEntities(allEntities, safetyFilters); + const entities = findEntities(allEntities, securityFilters); const floorCount = home.floors.length + (home.areas.length ? 1 : 0); @@ -164,7 +164,7 @@ export class SafetyViewStrategy extends ReactiveElement { ], }; - const areaCards = processAreasForSafety(areaIds, hass, entities); + const areaCards = processAreasForSecurity(areaIds, hass, entities); if (areaCards.length > 0) { section.cards!.push(...areaCards); @@ -188,7 +188,7 @@ export class SafetyViewStrategy extends ReactiveElement { ], }; - const areaCards = processAreasForSafety(home.areas, hass, entities); + const areaCards = processAreasForSecurity(home.areas, hass, entities); if (areaCards.length > 0) { section.cards!.push(...areaCards); @@ -209,9 +209,9 @@ export class SafetyViewStrategy extends ReactiveElement { heading: sections.length > 0 ? hass.localize( - "ui.panel.lovelace.strategy.safety.other_devices" + "ui.panel.lovelace.strategy.security.other_devices" ) - : hass.localize("ui.panel.lovelace.strategy.safety.devices"), + : hass.localize("ui.panel.lovelace.strategy.security.devices"), }, ...unassignedCards, ], @@ -229,6 +229,6 @@ export class SafetyViewStrategy extends ReactiveElement { declare global { interface HTMLElementTagNameMap { - "safety-view-strategy": SafetyViewStrategy; + "security-view-strategy": SecurityViewStrategy; } } diff --git a/src/translations/en.json b/src/translations/en.json index 6ae6ff6cde..e4be08ea0a 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -12,7 +12,7 @@ "media_browser": "Media", "profile": "Profile", "light": "Lights", - "safety": "Safety", + "security": "Security", "climate": "Climate" }, "state": { @@ -6955,7 +6955,7 @@ "lights": "Lights", "other_lights": "Other lights" }, - "safety": { + "security": { "devices": "Devices", "other_devices": "Other devices" }, From 3ba6bf272eeccd2cef017dd5914995ff9e42b71e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 4 Nov 2025 18:21:11 +0100 Subject: [PATCH 33/66] Bumped version to 20251104.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6769fc6f66..4121f45f83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20251103.0" +version = "20251104.0" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend" From 435c82489b840c4e3252a03afa2f9e74bb2611cc Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Wed, 5 Nov 2025 14:48:18 +0100 Subject: [PATCH 34/66] Fix assist conversation language picker (#27764) --- .../assist-pipeline-detail-conversation.ts | 17 ++++++++++++++--- .../dialog-voice-assistant-pipeline-detail.ts | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-conversation.ts b/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-conversation.ts index dac5f01850..5749aa607e 100644 --- a/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-conversation.ts +++ b/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-conversation.ts @@ -109,15 +109,26 @@ export class AssistPipelineDetailConversation extends LitElement { } private _supportedLanguagesChanged(ev) { - if (ev.detail.value === "*") { + this._supportedLanguages = ev.detail.value; + + if ( + this._supportedLanguages === "*" || + !this._supportedLanguages?.includes( + this.data?.conversation_language || "" + ) || + !this.data?.conversation_language + ) { // wait for update of conversation_engine setTimeout(() => { const value = { ...this.data }; - value.conversation_language = "*"; + if (this._supportedLanguages === "*") { + value.conversation_language = "*"; + } else { + value.conversation_language = this._supportedLanguages?.[0] ?? null; + } fireEvent(this, "value-changed", { value }); }, 0); } - this._supportedLanguages = ev.detail.value; } static styles = css` diff --git a/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts b/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts index 9786c6a89b..5a65b3d96b 100644 --- a/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts +++ b/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts @@ -214,7 +214,7 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { ${this._params.pipeline?.id From 27beab3133b54d40274edaac64ab84388f0fddb6 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 5 Nov 2025 08:33:11 +0100 Subject: [PATCH 35/66] Fix z-index for target picker item row icon (#27798) --- src/components/target-picker/ha-target-picker-item-row.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/target-picker/ha-target-picker-item-row.ts b/src/components/target-picker/ha-target-picker-item-row.ts index f7a5db23c8..e33be98842 100644 --- a/src/components/target-picker/ha-target-picker-item-row.ts +++ b/src/components/target-picker/ha-target-picker-item-row.ts @@ -638,6 +638,7 @@ export class HaTargetPickerItemRow extends LitElement { img { width: 24px; height: 24px; + z-index: 1; } ha-icon-button { --mdc-icon-button-size: 32px; From afb2ad95a4e2564cedc7790b3e19f6298432d959 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Wed, 5 Nov 2025 10:03:56 +0100 Subject: [PATCH 36/66] Fix target picker in card editor (#27800) --- src/components/ha-target-picker.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ha-target-picker.ts b/src/components/ha-target-picker.ts index b318b842f0..114d95a887 100644 --- a/src/components/ha-target-picker.ts +++ b/src/components/ha-target-picker.ts @@ -347,7 +347,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { this._pickerFilter = filter; }; - private _hidePicker() { + private _hidePicker(ev) { + ev.stopPropagation(); this._open = false; this._pickerWrapperOpen = false; From 5f0cf1b5225a984a35172a72e8b82f3baf42d05f Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Wed, 5 Nov 2025 10:47:46 +0100 Subject: [PATCH 37/66] Add condition/action dialog: blocks title (#27801) --- src/panels/config/automation/add-automation-element-dialog.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/panels/config/automation/add-automation-element-dialog.ts b/src/panels/config/automation/add-automation-element-dialog.ts index bb946abeff..091b9a0045 100644 --- a/src/panels/config/automation/add-automation-element-dialog.ts +++ b/src/panels/config/automation/add-automation-element-dialog.ts @@ -895,7 +895,9 @@ class DialogAddAutomationElement return html`
- ${title} + ${this._tab === "blocks" && !this._filter + ? this.hass.localize("ui.panel.config.automation.editor.blocks") + : title}
Date: Wed, 5 Nov 2025 10:47:20 +0100 Subject: [PATCH 38/66] Add trigger/condition/action dialog: fix empty elements in search results (#27802) --- src/panels/config/automation/add-automation-element-dialog.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panels/config/automation/add-automation-element-dialog.ts b/src/panels/config/automation/add-automation-element-dialog.ts index 091b9a0045..00de14faec 100644 --- a/src/panels/config/automation/add-automation-element-dialog.ts +++ b/src/panels/config/automation/add-automation-element-dialog.ts @@ -257,7 +257,7 @@ class DialogAddAutomationElement const results = fuse.multiTermsSearch(filter); if (results) { - return results.map((result) => result.item); + return results.map((result) => result.item).filter((item) => item.name); } return items; } @@ -294,7 +294,7 @@ class DialogAddAutomationElement const results = fuse.multiTermsSearch(filter); if (results) { - return results.map((result) => result.item); + return results.map((result) => result.item).filter((item) => item.name); } return items; } From cdfb7f914f1106a5d76f1ff609f2a0169507f50c Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 5 Nov 2025 12:57:34 +0100 Subject: [PATCH 39/66] Fix target picker in logbook card editor (#27804) Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com> --- src/components/ha-target-picker.ts | 332 ++++++++++-------- .../hui-logbook-card-editor.ts | 22 +- 2 files changed, 201 insertions(+), 153 deletions(-) diff --git a/src/components/ha-target-picker.ts b/src/components/ha-target-picker.ts index 114d95a887..d2eae714b4 100644 --- a/src/components/ha-target-picker.ts +++ b/src/components/ha-target-picker.ts @@ -87,166 +87,208 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { protected render() { if (this.addOnTop) { - return html` ${this._renderChips()} ${this._renderItems()} `; + return html` ${this._renderPicker()} ${this._renderItems()} `; } - return html` ${this._renderItems()} ${this._renderChips()} `; + return html` ${this._renderItems()} ${this._renderPicker()} `; } private _renderValueChips() { - return html`
- ${this.value?.floor_id - ? ensureArray(this.value.floor_id).map( - (floor_id) => html` - - ` - ) - : nothing} - ${this.value?.area_id - ? ensureArray(this.value.area_id).map( - (area_id) => html` - - ` - ) - : nothing} - ${this.value?.device_id - ? ensureArray(this.value.device_id).map( - (device_id) => html` - - ` - ) - : nothing} - ${this.value?.entity_id - ? ensureArray(this.value.entity_id).map( - (entity_id) => html` - - ` - ) - : nothing} - ${this.value?.label_id - ? ensureArray(this.value.label_id).map( - (label_id) => html` - - ` - ) - : nothing} -
`; - } + const entityIds = this.value?.entity_id + ? ensureArray(this.value.entity_id) + : []; + const deviceIds = this.value?.device_id + ? ensureArray(this.value.device_id) + : []; + const areaIds = this.value?.area_id ? ensureArray(this.value.area_id) : []; + const floorIds = this.value?.floor_id + ? ensureArray(this.value.floor_id) + : []; + const labelIds = this.value?.label_id + ? ensureArray(this.value.label_id) + : []; - private _renderValueGroups() { - return html`
- ${this.value?.entity_id - ? html` - - - ` - : nothing} - ${this.value?.device_id - ? html` - - - ` - : nothing} - ${this.value?.floor_id || this.value?.area_id - ? html` - - - ` - : nothing} - ${this.value?.label_id - ? html` - - - ` - : nothing} -
`; - } - - private _renderItems() { if ( - !this.value?.floor_id && - !this.value?.area_id && - !this.value?.device_id && - !this.value?.entity_id && - !this.value?.label_id + !entityIds.length && + !deviceIds.length && + !areaIds.length && + !floorIds.length && + !labelIds.length ) { return nothing; } + return html` +
+ ${floorIds.length + ? floorIds.map( + (floor_id) => html` + + ` + ) + : nothing} + ${areaIds.length + ? areaIds.map( + (area_id) => html` + + ` + ) + : nothing} + ${deviceIds.length + ? deviceIds.map( + (device_id) => html` + + ` + ) + : nothing} + ${entityIds.length + ? entityIds.map( + (entity_id) => html` + + ` + ) + : nothing} + ${labelIds.length + ? labelIds.map( + (label_id) => html` + + ` + ) + : nothing} +
+ `; + } + + private _renderValueGroups() { + const entityIds = this.value?.entity_id + ? ensureArray(this.value.entity_id) + : []; + const deviceIds = this.value?.device_id + ? ensureArray(this.value.device_id) + : []; + const areaIds = this.value?.area_id ? ensureArray(this.value.area_id) : []; + const floorIds = this.value?.floor_id + ? ensureArray(this.value.floor_id) + : []; + const labelIds = this.value?.label_id + ? ensureArray(this.value?.label_id) + : []; + + if ( + !entityIds.length && + !deviceIds.length && + !areaIds.length && + !floorIds.length && + !labelIds.length + ) { + return nothing; + } + + return html` +
+ ${entityIds.length + ? html` + + + ` + : nothing} + ${deviceIds.length + ? html` + + + ` + : nothing} + ${floorIds.length || areaIds.length + ? html` + + + ` + : nothing} + ${labelIds.length + ? html` + + + ` + : nothing} +
+ `; + } + + private _renderItems() { return html` ${this.compact ? this._renderValueChips() : this._renderValueGroups()} `; } - private _renderChips() { + private _renderPicker() { return html`
`; @@ -148,6 +147,13 @@ export class HuiLogbookCardEditor ); } }; + + static styles = css` + ha-target-picker { + display: block; + margin-top: var(--ha-space-4); + } + `; } declare global { From afd91b226153816817ac168ab92a955402a86d7a Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Wed, 5 Nov 2025 10:54:36 +0100 Subject: [PATCH 40/66] Fix auth language picker styles (#27805) --- landing-page/src/ha-landing-page.ts | 50 +++++++++++----------------- src/auth/ha-authorize.ts | 33 +++++++----------- src/components/ha-language-picker.ts | 47 ++++++++++++++++++++------ 3 files changed, 68 insertions(+), 62 deletions(-) diff --git a/landing-page/src/ha-landing-page.ts b/landing-page/src/ha-landing-page.ts index a8601b82b8..b1d64b02e5 100644 --- a/landing-page/src/ha-landing-page.ts +++ b/landing-page/src/ha-landing-page.ts @@ -1,22 +1,25 @@ import "@material/mwc-linear-progress"; -import { type PropertyValues, css, html, nothing } from "lit"; +import { mdiOpenInNew } from "@mdi/js"; +import { css, html, nothing, type PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { extractSearchParam } from "../../src/common/url/search-params"; import "../../src/components/ha-alert"; +import "../../src/components/ha-button"; import "../../src/components/ha-fade-in"; import "../../src/components/ha-spinner"; -import { haStyle } from "../../src/resources/styles"; -import "../../src/onboarding/onboarding-welcome-links"; -import "./components/landing-page-network"; -import "./components/landing-page-logs"; -import { extractSearchParam } from "../../src/common/url/search-params"; -import { onBoardingStyles } from "../../src/onboarding/styles"; +import "../../src/components/ha-svg-icon"; import { makeDialogManager } from "../../src/dialogs/make-dialog-manager"; -import { LandingPageBaseElement } from "./landing-page-base-element"; +import "../../src/onboarding/onboarding-welcome-links"; +import { onBoardingStyles } from "../../src/onboarding/styles"; +import { haStyle } from "../../src/resources/styles"; +import "./components/landing-page-logs"; +import "./components/landing-page-network"; import { getSupervisorNetworkInfo, pingSupervisor, type NetworkInfo, } from "./data/supervisor"; +import { LandingPageBaseElement } from "./landing-page-base-element"; export const ASSUME_CORE_START_SECONDS = 60; const SCHEDULE_CORE_CHECK_SECONDS = 1; @@ -94,16 +97,21 @@ class HaLandingPage extends LandingPageBaseElement { - ${this.localize("ui.panel.page-onboarding.help")} + ${this.localize("ui.panel.page-onboarding.help")} + +
`; } @@ -218,26 +226,8 @@ class HaLandingPage extends LandingPageBaseElement { ha-alert p { text-align: unset; } - ha-language-picker { - display: block; - width: 200px; - border-radius: var(--ha-border-radius-sm); - overflow: hidden; - --ha-select-height: 40px; - --mdc-select-fill-color: none; - --mdc-select-label-ink-color: var(--primary-text-color, #212121); - --mdc-select-ink-color: var(--primary-text-color, #212121); - --mdc-select-idle-line-color: transparent; - --mdc-select-hover-line-color: transparent; - --mdc-select-dropdown-icon-color: var(--primary-text-color, #212121); - --mdc-shape-small: 0; - } - a { - text-decoration: none; - color: var(--primary-text-color); - margin-right: 16px; - margin-inline-end: 16px; - margin-inline-start: initial; + .footer ha-svg-icon { + --mdc-icon-size: var(--ha-space-5); } ha-fade-in { min-height: calc(100vh - 64px - 88px); diff --git a/src/auth/ha-authorize.ts b/src/auth/ha-authorize.ts index 56a0143031..294c0201bd 100644 --- a/src/auth/ha-authorize.ts +++ b/src/auth/ha-authorize.ts @@ -1,4 +1,5 @@ /* eslint-disable lit/prefer-static-styles */ +import { mdiOpenInNew } from "@mdi/js"; import type { PropertyValues } from "lit"; import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -6,6 +7,8 @@ import punycode from "punycode"; import { applyThemesOnElement } from "../common/dom/apply_themes_on_element"; import { extractSearchParamsObject } from "../common/url/search-params"; import "../components/ha-alert"; +import "../components/ha-button"; +import "../components/ha-svg-icon"; import type { AuthProvider, AuthUrlSearchParams } from "../data/auth"; import { fetchAuthProviders } from "../data/auth"; import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin"; @@ -133,25 +136,8 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) { justify-content: space-between; align-items: center; } - ha-language-picker { - width: 200px; - border-radius: var(--ha-border-radius-sm); - overflow: hidden; - --ha-select-height: 40px; - --mdc-select-fill-color: none; - --mdc-select-label-ink-color: var(--primary-text-color, #212121); - --mdc-select-ink-color: var(--primary-text-color, #212121); - --mdc-select-idle-line-color: transparent; - --mdc-select-hover-line-color: transparent; - --mdc-select-dropdown-icon-color: var(--primary-text-color, #212121); - --mdc-shape-small: 0; - } - .footer a { - text-decoration: none; - color: var(--primary-text-color); - margin-right: 16px; - margin-inline-end: 16px; - margin-inline-start: initial; + .footer ha-svg-icon { + --mdc-icon-size: var(--ha-space-5); } h1 { font-size: var(--ha-font-size-3xl); @@ -205,16 +191,21 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) { - ${this.localize("ui.panel.page-authorize.help")} + ${this.localize("ui.panel.page-authorize.help")} + +
`; } diff --git a/src/components/ha-language-picker.ts b/src/components/ha-language-picker.ts index f0f6d4449d..40fb2a039c 100644 --- a/src/components/ha-language-picker.ts +++ b/src/components/ha-language-picker.ts @@ -1,6 +1,7 @@ +import { mdiMenuDown } from "@mdi/js"; import type { PropertyValues } from "lit"; -import { css, html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../common/dom/fire_event"; import { formatLanguageCode } from "../common/language/format_language"; @@ -8,10 +9,10 @@ import { caseInsensitiveStringCompare } from "../common/string/compare"; import type { FrontendLocaleData } from "../data/translation"; import { translationMetadata } from "../resources/translations-metadata"; import type { HomeAssistant, ValueChangedEvent } from "../types"; +import "./ha-button"; import "./ha-generic-picker"; -import "./ha-list-item"; +import type { HaGenericPicker } from "./ha-generic-picker"; import type { PickerComboBoxItem } from "./ha-picker-combo-box"; -import "./ha-select"; export const getLanguageOptions = ( languages: string[], @@ -75,6 +76,9 @@ export class HaLanguagePicker extends LitElement { @property({ attribute: "native-name", type: Boolean }) public nativeName = false; + @property({ type: Boolean, attribute: "button-style" }) + public buttonStyle = false; + @property({ attribute: "no-sort", type: Boolean }) public noSort = false; @property({ attribute: "inline-arrow", type: Boolean }) @@ -82,6 +86,8 @@ export class HaLanguagePicker extends LitElement { @state() _defaultLanguages: string[] = []; + @query("ha-generic-picker", true) public genericPicker!: HaGenericPicker; + protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); this._computeDefaultLanguageOptions(); @@ -101,12 +107,13 @@ export class HaLanguagePicker extends LitElement { this.hass?.locale ); - private _valueRenderer = (value) => { - const language = this._getItems().find( - (lang) => lang.id === value - )?.primary; - return html`${language ?? value} `; - }; + private _getLanguageName = (lang?: string) => + this._getItems().find((language) => language.id === lang)?.primary; + + private _valueRenderer = (value) => + html`${this._getLanguageName(value) ?? value} `; protected render() { const value = @@ -130,10 +137,28 @@ export class HaLanguagePicker extends LitElement { .getItems=${this._getItems} @value-changed=${this._changed} hide-clear-icon - > + > + ${this.buttonStyle + ? html` + ${this._getLanguageName(value)} + + ` + : nothing} + `; } + private _openPicker(ev: Event) { + ev.stopPropagation(); + this.genericPicker.open(); + } + static styles = css` ha-generic-picker { width: 100%; From 1ec432a20f56b8a2318aae83ad8a32ce9f57236e Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Wed, 5 Nov 2025 14:31:25 +0100 Subject: [PATCH 41/66] Change add trigger/condition/action dialog title (#27811) Change add dialog title --- src/panels/config/automation/add-automation-element-dialog.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/config/automation/add-automation-element-dialog.ts b/src/panels/config/automation/add-automation-element-dialog.ts index 00de14faec..fdad414a16 100644 --- a/src/panels/config/automation/add-automation-element-dialog.ts +++ b/src/panels/config/automation/add-automation-element-dialog.ts @@ -678,7 +678,7 @@ class DialogAddAutomationElement ); const typeTitle = this.hass.localize( - `ui.panel.config.automation.editor.${automationElementType}s.header` + `ui.panel.config.automation.editor.${automationElementType}s.add` ); const tabButtons = [ From 2d36a0d37f5c813fb55b346c0868d2087264f13e Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Wed, 5 Nov 2025 14:27:53 +0100 Subject: [PATCH 42/66] Add trigger/condition/action dialog - Show device group always on top (#27812) add automation element dialog Device always on top --- .../automation/add-automation-element-dialog.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/panels/config/automation/add-automation-element-dialog.ts b/src/panels/config/automation/add-automation-element-dialog.ts index fdad414a16..07e26abde0 100644 --- a/src/panels/config/automation/add-automation-element-dialog.ts +++ b/src/panels/config/automation/add-automation-element-dialog.ts @@ -383,9 +383,16 @@ class DialogAddAutomationElement generatedCollections.push({ titleKey: collection.titleKey, - groups: groups.sort((a, b) => - stringCompare(a.name, b.name, this.hass.locale.language) - ), + groups: groups.sort((a, b) => { + // make sure device is always on top + if (a.key === "device" || a.key === "device_id") { + return -1; + } + if (b.key === "device" || b.key === "device_id") { + return 1; + } + return stringCompare(a.name, b.name, this.hass.locale.language); + }), }); }); return generatedCollections; From 616237caeee955f11a1353da9b133e1c3f246ca4 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Wed, 5 Nov 2025 14:56:58 +0100 Subject: [PATCH 43/66] Fix target picker with empty sections (#27813) --- src/components/target-picker/ha-target-picker-selector.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/target-picker/ha-target-picker-selector.ts b/src/components/target-picker/ha-target-picker-selector.ts index b340e7d36c..94a3db5155 100644 --- a/src/components/target-picker/ha-target-picker-selector.ts +++ b/src/components/target-picker/ha-target-picker-selector.ts @@ -705,7 +705,7 @@ export class HaTargetPickerSelector extends LitElement { ) as EntityComboBoxItem[]; } - if (!filterType) { + if (!filterType && entities.length) { // show group title items.push( this.hass.localize("ui.components.target-picker.type.entities") @@ -733,7 +733,7 @@ export class HaTargetPickerSelector extends LitElement { devices = this._filterGroup("device", devices); } - if (!filterType) { + if (!filterType && devices.length) { // show group title items.push( this.hass.localize("ui.components.target-picker.type.devices") @@ -769,7 +769,7 @@ export class HaTargetPickerSelector extends LitElement { ) as FloorComboBoxItem[]; } - if (!filterType) { + if (!filterType && areasAndFloors.length) { // show group title items.push( this.hass.localize("ui.components.target-picker.type.areas") @@ -811,7 +811,7 @@ export class HaTargetPickerSelector extends LitElement { labels = this._filterGroup("label", labels); } - if (!filterType) { + if (!filterType && labels.length) { // show group title items.push( this.hass.localize("ui.components.target-picker.type.labels") From 9c42c8bbc4bd86f00eef0fd81d6c7fe5a7fa5048 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:14:41 +0100 Subject: [PATCH 44/66] Add fallback icon for domain template (#27814) --- src/data/icons.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/data/icons.ts b/src/data/icons.ts index c42b8e0f21..2384d45ab7 100644 --- a/src/data/icons.ts +++ b/src/data/icons.ts @@ -12,6 +12,7 @@ import { mdiChatSleep, mdiClipboardList, mdiClock, + mdiCodeBraces, mdiCog, mdiCommentAlert, mdiCounter, @@ -113,6 +114,7 @@ export const FALLBACK_DOMAIN_ICONS = { text: mdiFormTextbox, time: mdiClock, timer: mdiTimerOutline, + template: mdiCodeBraces, todo: mdiClipboardList, tts: mdiSpeakerMessage, vacuum: mdiRobotVacuum, From 89796e425a0019091d792742af7dfc098fd4ba62 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 5 Nov 2025 15:26:35 +0100 Subject: [PATCH 45/66] Bumped version to 20251105.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4121f45f83..3a82f795c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20251104.0" +version = "20251105.0" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend" From ab4c6f80f4fb8da71918dce8c578c00aba89c494 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Fri, 7 Nov 2025 09:34:42 +0200 Subject: [PATCH 46/66] Disable graph resize animation for general resizing (#27816) --- src/components/chart/ha-chart-base.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index f4d8f31c93..01cec98dcb 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -35,7 +35,6 @@ export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; const LEGEND_OVERFLOW_LIMIT = 10; const LEGEND_OVERFLOW_LIMIT_MOBILE = 6; const DOUBLE_TAP_TIME = 300; -const RESIZE_ANIMATION_DURATION = 250; export type CustomLegendOption = ECOption["legend"] & { type: "custom"; @@ -91,6 +90,8 @@ export class HaChartBase extends LitElement { private _shouldResizeChart = false; + private _resizeAnimationDuration?: number; + // @ts-ignore private _resizeController = new ResizeController(this, { callback: () => { @@ -214,6 +215,7 @@ export class HaChartBase extends LitElement { ) { // custom legend changes may require a resize to layout properly this._shouldResizeChart = true; + this._resizeAnimationDuration = 250; } } else if (this._isTouchDevice && changedProps.has("_isZoomed")) { chartOptions.dataZoom = this._getDataZoomConfig(); @@ -977,11 +979,14 @@ export class HaChartBase extends LitElement { private _handleChartRenderFinished = () => { if (this._shouldResizeChart) { this.chart?.resize({ - animation: this._reducedMotion - ? undefined - : { duration: RESIZE_ANIMATION_DURATION }, + animation: + this._reducedMotion || + typeof this._resizeAnimationDuration !== "number" + ? undefined + : { duration: this._resizeAnimationDuration }, }); this._shouldResizeChart = false; + this._resizeAnimationDuration = undefined; } }; From bb0813333d7a0bc4447bf9379e382d43fb44612e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 5 Nov 2025 16:13:48 +0100 Subject: [PATCH 47/66] Fix landing page build (#27817) --- build-scripts/bundle.cjs | 7 ++++--- build-scripts/rspack.cjs | 5 ++++- src/components/ha-generic-picker.ts | 1 - 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/build-scripts/bundle.cjs b/build-scripts/bundle.cjs index eadc3001d3..98fb78ce13 100644 --- a/build-scripts/bundle.cjs +++ b/build-scripts/bundle.cjs @@ -18,16 +18,16 @@ module.exports.sourceMapURL = () => { module.exports.ignorePackages = () => []; // Files from NPM packages that we should replace with empty file -module.exports.emptyPackages = ({ isHassioBuild }) => +module.exports.emptyPackages = ({ isHassioBuild, isLandingPageBuild }) => [ require.resolve("@vaadin/vaadin-material-styles/typography.js"), require.resolve("@vaadin/vaadin-material-styles/font-icons.js"), // Icons in supervisor conflict with icons in HA so we don't load. - isHassioBuild && + (isHassioBuild || isLandingPageBuild) && require.resolve( path.resolve(paths.root_dir, "src/components/ha-icon.ts") ), - isHassioBuild && + (isHassioBuild || isLandingPageBuild) && require.resolve( path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts") ), @@ -337,6 +337,7 @@ module.exports.config = { publicPath: publicPath(latestBuild), isProdBuild, latestBuild, + isLandingPageBuild: true, }; }, }; diff --git a/build-scripts/rspack.cjs b/build-scripts/rspack.cjs index 34acb8c5b8..76f85214f6 100644 --- a/build-scripts/rspack.cjs +++ b/build-scripts/rspack.cjs @@ -41,6 +41,7 @@ const createRspackConfig = ({ isStatsBuild, isTestBuild, isHassioBuild, + isLandingPageBuild, dontHash, }) => { if (!dontHash) { @@ -168,7 +169,9 @@ const createRspackConfig = ({ }, }), new rspack.NormalModuleReplacementPlugin( - new RegExp(bundle.emptyPackages({ isHassioBuild }).join("|")), + new RegExp( + bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).join("|") + ), path.resolve(paths.root_dir, "src/util/empty.js") ), !isProdBuild && new LogStartCompilePlugin(), diff --git a/src/components/ha-generic-picker.ts b/src/components/ha-generic-picker.ts index 256d4c29b4..06abc08bc0 100644 --- a/src/components/ha-generic-picker.ts +++ b/src/components/ha-generic-picker.ts @@ -10,7 +10,6 @@ import type { HomeAssistant } from "../types"; import "./ha-bottom-sheet"; import "./ha-button"; import "./ha-combo-box-item"; -import "./ha-icon-button"; import "./ha-input-helper-text"; import "./ha-picker-combo-box"; import type { From e842193cd6aeb528b05bd5d3eb5743142c9e2c74 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 6 Nov 2025 15:15:52 +0100 Subject: [PATCH 48/66] Fix index for service action translation in service action dialog (#27824) --- src/panels/lovelace/common/handle-action.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/lovelace/common/handle-action.ts b/src/panels/lovelace/common/handle-action.ts index 68418e6982..56c60ccae3 100644 --- a/src/panels/lovelace/common/handle-action.ts +++ b/src/panels/lovelace/common/handle-action.ts @@ -67,7 +67,7 @@ export const handleAction = async ( await hass.loadBackendTranslation("title"); const localize = await hass.loadBackendTranslation("services"); serviceName = `${domainToName(localize, domain)}: ${ - localize(`component.${domain}.services.${serviceName}.name`) || + localize(`component.${domain}.services.${service}.name`) || serviceDomains[domain][service].name || service }`; From a8b6e5aa3d7cde6a3e2ba79b9de51d77e784b0fd Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Thu, 6 Nov 2025 14:46:53 +0100 Subject: [PATCH 49/66] Add trigger/condition/action dialog: select single search result with enter key (#27825) * Add trigger/condition/action dialog: select single search result with enter key * Update src/panels/config/automation/add-automation-element-dialog.ts --------- Co-authored-by: Petar Petrov --- .../add-automation-element-dialog.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/panels/config/automation/add-automation-element-dialog.ts b/src/panels/config/automation/add-automation-element-dialog.ts index 07e26abde0..96fd43225b 100644 --- a/src/panels/config/automation/add-automation-element-dialog.ts +++ b/src/panels/config/automation/add-automation-element-dialog.ts @@ -1054,6 +1054,7 @@ class DialogAddAutomationElement private _onSearchFocus(ev) { this._removeKeyboardShortcuts = tinykeys(ev.target, { ArrowDown: this._focusSearchList, + Enter: this._pickSingleItem, }); } @@ -1070,6 +1071,39 @@ class DialogAddAutomationElement this._itemsListFirstElement.focus(); }; + private _pickSingleItem = (ev: KeyboardEvent) => { + if (!this._filter) { + return; + } + + ev.preventDefault(); + const automationElementType = this._params!.type; + + const items = [ + ...this._getFilteredItems( + automationElementType, + this._filter, + this.hass.localize, + this.hass.services, + this._manifests + ), + ...(automationElementType !== "trigger" + ? this._getFilteredBuildingBlocks( + automationElementType, + this._filter, + this.hass.localize + ) + : []), + ]; + + if (items.length !== 1) { + return; + } + + this._params!.add(items[0].key); + this.closeDialog(); + }; + static get styles(): CSSResultGroup { return [ css` From b4613edeb745f74a0defc7846a51876cfbea790a Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:15:09 +0100 Subject: [PATCH 50/66] Target picker row check if not found entity isn't "all" (#27826) Target picker row check if not found entity isn't all --- src/components/target-picker/ha-target-picker-item-row.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/target-picker/ha-target-picker-item-row.ts b/src/components/target-picker/ha-target-picker-item-row.ts index e33be98842..00782a3447 100644 --- a/src/components/target-picker/ha-target-picker-item-row.ts +++ b/src/components/target-picker/ha-target-picker-item-row.ts @@ -545,7 +545,7 @@ export class HaTargetPickerItemRow extends LitElement { name: entityName || deviceName || item, context, stateObject, - notFound: !stateObject, + notFound: !stateObject && item !== "all", }; } From e8cee84380e0b4ebc80d35362580522d6763cb4d Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Thu, 6 Nov 2025 11:42:30 +0100 Subject: [PATCH 51/66] Fix floor details area picker (#27827) --- src/components/ha-area-picker.ts | 3 +++ .../areas/dialog-floor-registry-detail.ts | 21 +++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts index 8777c513b9..7c96ed2d90 100644 --- a/src/components/ha-area-picker.ts +++ b/src/components/ha-area-picker.ts @@ -87,6 +87,8 @@ export class HaAreaPicker extends LitElement { @property({ type: Boolean }) public required = false; + @property({ attribute: "add-button-label" }) public addButtonLabel?: string; + @query("ha-generic-picker") private _picker?: HaGenericPicker; public async open() { @@ -375,6 +377,7 @@ export class HaAreaPicker extends LitElement { .getItems=${this._getItems} .getAdditionalItems=${this._getAdditionalItems} .valueRenderer=${valueRenderer} + .addButtonLabel=${this.addButtonLabel} @value-changed=${this._valueChanged} > diff --git a/src/panels/config/areas/dialog-floor-registry-detail.ts b/src/panels/config/areas/dialog-floor-registry-detail.ts index 3642df35fb..bafd9bf73c 100644 --- a/src/panels/config/areas/dialog-floor-registry-detail.ts +++ b/src/panels/config/areas/dialog-floor-registry-detail.ts @@ -8,24 +8,24 @@ import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/chips/ha-chip-set"; import "../../../components/chips/ha-input-chip"; import "../../../components/ha-alert"; -import "../../../components/ha-button"; import "../../../components/ha-aliases-editor"; +import "../../../components/ha-area-picker"; +import "../../../components/ha-button"; import { createCloseHeading } from "../../../components/ha-dialog"; import "../../../components/ha-icon-picker"; import "../../../components/ha-picture-upload"; import "../../../components/ha-settings-row"; import "../../../components/ha-svg-icon"; import "../../../components/ha-textfield"; -import "../../../components/ha-area-picker"; +import { updateAreaRegistryEntry } from "../../../data/area_registry"; import type { FloorRegistryEntry, FloorRegistryEntryMutableParams, } from "../../../data/floor_registry"; import { haStyle, haStyleDialog } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; -import type { FloorRegistryDetailDialogParams } from "./show-dialog-floor-registry-detail"; import { showAreaRegistryDetailDialog } from "./show-dialog-area-registry-detail"; -import { updateAreaRegistryEntry } from "../../../data/area_registry"; +import type { FloorRegistryDetailDialogParams } from "./show-dialog-floor-registry-detail"; class DialogFloorDetail extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -168,11 +168,6 @@ class DialogFloorDetail extends LitElement { )} -

- ${this.hass.localize( - "ui.panel.config.floors.editor.areas_description" - )} -

${areas.length ? html` ${repeat( @@ -197,13 +192,17 @@ class DialogFloorDetail extends LitElement { ` )} ` - : nothing} + : html`

+ ${this.hass.localize( + "ui.panel.config.floors.editor.areas_description" + )} +

`} a.area_id)} - .label=${this.hass.localize( + .addButtonLabel=${this.hass.localize( "ui.panel.config.floors.editor.add_area" )} > From 6fea535fdc03d7bd05cdb4c10c52c16ebf7f8b86 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 6 Nov 2025 11:39:52 +0100 Subject: [PATCH 52/66] Fix OHF logo theme (#27830) --- src/panels/config/info/ha-config-info.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/panels/config/info/ha-config-info.ts b/src/panels/config/info/ha-config-info.ts index a97e6310c1..908492310f 100644 --- a/src/panels/config/info/ha-config-info.ts +++ b/src/panels/config/info/ha-config-info.ts @@ -107,6 +107,8 @@ class HaConfigInfo extends LitElement { const customUiList: { name: string; url: string; version: string }[] = (window as any).CUSTOM_UI_LIST || []; + const isDark = this.hass.themes?.darkMode || false; + return html` - +
${this.hass.localize("ui.panel.config.info.proud_part_of")}
@@ -346,6 +348,10 @@ class HaConfigInfo extends LitElement { max-width: 250px; } + .ohf.dark img { + color-scheme: dark; + } + .versions { display: flex; flex-direction: column; From c1787ab9941ead0c0cc8a7775cb563705cf97395 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 7 Nov 2025 12:43:00 +0100 Subject: [PATCH 53/66] Fix backup download and delete actions (#27851) --- src/panels/config/backup/ha-config-backup-backups.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/panels/config/backup/ha-config-backup-backups.ts b/src/panels/config/backup/ha-config-backup-backups.ts index 382abe6a1f..552c40b7db 100644 --- a/src/panels/config/backup/ha-config-backup-backups.ts +++ b/src/panels/config/backup/ha-config-backup-backups.ts @@ -569,15 +569,15 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { navigate(`/config/backup/details/${id}`); } - private async _downloadBackup(ev): Promise { + private _downloadBackup = async (ev): Promise => { const backup = ev.parentElement.anchorElement.backup; if (!backup) { return; } downloadBackup(this.hass, this, backup, this.config); - } + }; - private async _deleteBackup(ev): Promise { + private _deleteBackup = async (ev): Promise => { const backup = ev.parentElement.anchorElement.backup; if (!backup) { return; @@ -609,7 +609,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { return; } fireEvent(this, "ha-refresh-backup-info"); - } + }; private async _deleteSelected() { const confirm = await showConfirmationDialog(this, { From 5c25a63ea589b9f7b02f0b0bc3c48b5dd35afe26 Mon Sep 17 00:00:00 2001 From: Yuksel Beyti Date: Sun, 9 Nov 2025 12:56:43 +0100 Subject: [PATCH 54/66] Fix malformed HTML tags in backup backups component (#27872) --- .../config/backup/ha-config-backup-backups.ts | 130 ++++++++---------- 1 file changed, 61 insertions(+), 69 deletions(-) diff --git a/src/panels/config/backup/ha-config-backup-backups.ts b/src/panels/config/backup/ha-config-backup-backups.ts index 552c40b7db..0246ad06a8 100644 --- a/src/panels/config/backup/ha-config-backup-backups.ts +++ b/src/panels/config/backup/ha-config-backup-backups.ts @@ -372,16 +372,14 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { clickable id="backup_id" has-filters - .filters=${ - Object.values(this._filters).filter((filter) => - Array.isArray(filter) - ? filter.length - : filter && - Object.values(filter).some((val) => - Array.isArray(val) ? val.length : val - ) - ).length - } + .filters=${Object.values(this._filters).filter((filter) => + Array.isArray(filter) + ? filter.length + : filter && + Object.values(filter).some((val) => + Array.isArray(val) ? val.length : val + ) + ).length} selectable .selected=${this._selected.length} .initialGroupColumn=${this._activeGrouping} @@ -423,30 +421,28 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
- ${ - !this.narrow - ? html` - - ${this.hass.localize( - "ui.panel.config.backup.backups.delete_selected" - )} - - ` - : html` - - ` - } + ${!this.narrow + ? html` + + ${this.hass.localize( + "ui.panel.config.backup.backups.delete_selected" + )} + + ` + : html` + + `}
- ${ - !this._needsOnboarding - ? html` - - ${backupInProgress - ? html`
- -
` - : html``} -
- ` - : nothing - } + ${!this._needsOnboarding + ? html` + + ${backupInProgress + ? html`
+ +
` + : html``} +
+ ` + : nothing} - - - ${this.hass.localize("ui.common.download")} - - - - ${this.hass.localize("ui.common.delete")} - - - > - + + + ${this.hass.localize("ui.common.download")} + + + + ${this.hass.localize("ui.common.delete")} + + `; } From dc8f1211e6f45939f831c15a2dd030ee4d57f4ac Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Mon, 10 Nov 2025 04:09:44 -0800 Subject: [PATCH 55/66] Fix entity editor with non-existant entity (#27875) --- .../lovelace/components/hui-entity-editor.ts | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/panels/lovelace/components/hui-entity-editor.ts b/src/panels/lovelace/components/hui-entity-editor.ts index 36a0415d47..719ee263a7 100644 --- a/src/panels/lovelace/components/hui-entity-editor.ts +++ b/src/panels/lovelace/components/hui-entity-editor.ts @@ -39,29 +39,31 @@ export class HuiEntityEditor extends LitElement { private _renderItem(item: EntityConfig, index: number) { const stateObj = this.hass.states[item.entity]; - const useDeviceName = entityUseDeviceName( - stateObj, - this.hass.entities, - this.hass.devices - ); + const useDeviceName = + stateObj && + entityUseDeviceName(stateObj, this.hass.entities, this.hass.devices); const isRTL = computeRTL(this.hass); const primary = + (stateObj && + this.hass.formatEntityName( + stateObj, + useDeviceName ? { type: "device" } : { type: "entity" } + )) || + item.entity; + + const secondary = + stateObj && this.hass.formatEntityName( stateObj, - useDeviceName ? { type: "device" } : { type: "entity" } - ) || item.entity; - - const secondary = this.hass.formatEntityName( - stateObj, - useDeviceName - ? [{ type: "area" }] - : [{ type: "area" }, { type: "device" }], - { - separator: isRTL ? " ◂ " : " ▸ ", - } - ); + useDeviceName + ? [{ type: "area" }] + : [{ type: "area" }, { type: "device" }], + { + separator: isRTL ? " ◂ " : " ▸ ", + } + ); return html` From c7ae78c02fc2ae11201404433bcd050d6acaa387 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 10 Nov 2025 09:53:18 +0200 Subject: [PATCH 56/66] Fix chart label outline color (#27882) --- src/components/chart/ha-chart-base.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 01cec98dcb..1097dc6dbe 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -627,6 +627,10 @@ export class HaChartBase extends LitElement { } private _createTheme(style: CSSStyleDeclaration) { + const textBorderColor = + style.getPropertyValue("--ha-card-background") || + style.getPropertyValue("--card-background-color"); + const textBorderWidth = 2; return { color: getAllGraphColors(style), backgroundColor: "transparent", @@ -650,22 +654,22 @@ export class HaChartBase extends LitElement { graph: { label: { color: style.getPropertyValue("--primary-text-color"), - textBorderColor: style.getPropertyValue("--primary-background-color"), - textBorderWidth: 2, + textBorderColor, + textBorderWidth, }, }, pie: { label: { color: style.getPropertyValue("--primary-text-color"), - textBorderColor: style.getPropertyValue("--primary-background-color"), - textBorderWidth: 2, + textBorderColor, + textBorderWidth, }, }, sankey: { label: { color: style.getPropertyValue("--primary-text-color"), - textBorderColor: style.getPropertyValue("--primary-background-color"), - textBorderWidth: 2, + textBorderColor, + textBorderWidth, }, }, categoryAxis: { From e8c9ed0528657e4e815d5a759d4b4cad9297c5d6 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 10 Nov 2025 11:20:26 +0200 Subject: [PATCH 57/66] Dynamic total energy for pie chart (#27883) --- .../energy/hui-energy-devices-graph-card.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts index 8c1a7fec3b..08c291da9d 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts @@ -8,6 +8,7 @@ import memoizeOne from "memoize-one"; import type { BarSeriesOption, PieSeriesOption } from "echarts/charts"; import { PieChart } from "echarts/charts"; import type { ECElementEvent } from "echarts/types/dist/shared"; +import type { PieDataItemOption } from "echarts/types/src/chart/pie/PieSeries"; import { filterXSS } from "../../../../common/util/xss"; import { getGraphColorByIndex } from "../../../../common/color/colors"; import { formatNumber } from "../../../../common/number/format_number"; @@ -387,6 +388,7 @@ export class HuiEnergyDevicesGraphCard }); if (this._chartType === "pie") { + const pieChartData = chartData as NonNullable; const { summedData, compareSummedData } = getSummedData(energyData); const { consumption, compareConsumption } = computeConsumptionData( summedData, @@ -399,7 +401,10 @@ export class HuiEnergyDevicesGraphCard "from_battery" in summedData; const untracked = showUntracked ? totalUsed - - chartData.reduce((acc: number, d: any) => acc + d.value[0], 0) + pieChartData.reduce( + (acc: number, d) => acc + (d as PieDataItemOption).value![0], + 0 + ) : 0; if (untracked > 0) { const color = getEnergyColor( @@ -409,7 +414,7 @@ export class HuiEnergyDevicesGraphCard false, "--history-unknown-color" ); - chartData.push({ + pieChartData.push({ id: "untracked", value: [untracked, "untracked"] as any, name: this.hass.localize( @@ -442,13 +447,20 @@ export class HuiEnergyDevicesGraphCard } } } + const totalChart = pieChartData.reduce( + (acc: number, d) => + this._hiddenStats.includes((d as PieDataItemOption).id as string) + ? acc + : acc + (d as PieDataItemOption).value![0], + 0 + ); datasets.push({ type: "pie", radius: ["0%", compareData ? "30%" : "40%"], name: this.hass.localize( "ui.panel.lovelace.cards.energy.energy_devices_graph.total_energy_usage" ), - data: [totalUsed], + data: [totalChart], label: { show: true, position: "center", @@ -456,7 +468,7 @@ export class HuiEnergyDevicesGraphCard fontSize: computedStyle.getPropertyValue("--ha-font-size-l"), lineHeight: 24, fontWeight: "bold", - formatter: `{a}\n${formatNumber(totalUsed, this.hass.locale)} kWh`, + formatter: `{a}\n${formatNumber(totalChart, this.hass.locale)} kWh`, }, cursor: "default", itemStyle: { From 1f2b8047a6bc8d17622bfc71ce9ea744e8c3d585 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:04:30 +0100 Subject: [PATCH 58/66] Use ha-ripple in ha-md-list-item (#27889) --- src/components/ha-md-list-item.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/ha-md-list-item.ts b/src/components/ha-md-list-item.ts index 2440c57805..f897b1d4d5 100644 --- a/src/components/ha-md-list-item.ts +++ b/src/components/ha-md-list-item.ts @@ -1,7 +1,8 @@ import { ListItemEl } from "@material/web/list/internal/listitem/list-item"; import { styles } from "@material/web/list/internal/listitem/list-item-styles"; -import { css } from "lit"; +import { css, html, nothing, type TemplateResult } from "lit"; import { customElement } from "lit/decorators"; +import "./ha-ripple"; export const haMdListStyles = [ styles, @@ -25,6 +26,18 @@ export const haMdListStyles = [ @customElement("ha-md-list-item") export class HaMdListItem extends ListItemEl { static override styles = haMdListStyles; + + protected renderRipple(): TemplateResult | typeof nothing { + if (this.type === "text") { + return nothing; + } + + return html``; + } } declare global { From dc76a42aaa5a7ff616bdb31b964b52920eb67333 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 10 Nov 2025 15:23:26 +0200 Subject: [PATCH 59/66] Smooth sensor card more when "Show more detail" is disabled (#27891) * Smooth sensor card more when "Show more detail" is disabled * Set minimum sample points to 10 --- src/components/chart/down-sample.ts | 70 +++++++++++++------ .../lovelace/common/graph/coordinates.ts | 11 +-- .../header-footer/hui-graph-header-footer.ts | 10 ++- 3 files changed, 61 insertions(+), 30 deletions(-) diff --git a/src/components/chart/down-sample.ts b/src/components/chart/down-sample.ts index 47855592c1..85ccf9c135 100644 --- a/src/components/chart/down-sample.ts +++ b/src/components/chart/down-sample.ts @@ -6,7 +6,8 @@ export function downSampleLineData< data: T[] | undefined, maxDetails: number, minX?: number, - maxX?: number + maxX?: number, + useMean = false ): T[] { if (!data) { return []; @@ -17,15 +18,13 @@ export function downSampleLineData< const min = minX ?? getPointData(data[0]!)[0]; const max = maxX ?? getPointData(data[data.length - 1]!)[0]; const step = Math.ceil((max - min) / Math.floor(maxDetails)); - const frames = new Map< - number, - { - min: { point: (typeof data)[number]; x: number; y: number }; - max: { point: (typeof data)[number]; x: number; y: number }; - } - >(); // Group points into frames + const frames = new Map< + number, + { point: (typeof data)[number]; x: number; y: number }[] + >(); + for (const point of data) { const pointData = getPointData(point); if (!Array.isArray(pointData)) continue; @@ -36,28 +35,53 @@ export function downSampleLineData< const frameIndex = Math.floor((x - min) / step); const frame = frames.get(frameIndex); if (!frame) { - frames.set(frameIndex, { min: { point, x, y }, max: { point, x, y } }); + frames.set(frameIndex, [{ point, x, y }]); } else { - if (frame.min.y > y) { - frame.min = { point, x, y }; - } - if (frame.max.y < y) { - frame.max = { point, x, y }; - } + frame.push({ point, x, y }); } } // Convert frames back to points const result: T[] = []; - for (const [_i, frame] of frames) { - // Use min/max points to preserve visual accuracy - // The order of the data must be preserved so max may be before min - if (frame.min.x > frame.max.x) { - result.push(frame.max.point); + + if (useMean) { + // Use mean values for each frame + for (const [_i, framePoints] of frames) { + const sumY = framePoints.reduce((acc, p) => acc + p.y, 0); + const meanY = sumY / framePoints.length; + const sumX = framePoints.reduce((acc, p) => acc + p.x, 0); + const meanX = sumX / framePoints.length; + + const firstPoint = framePoints[0].point; + const pointData = getPointData(firstPoint); + const meanPoint = ( + Array.isArray(pointData) ? [meanX, meanY] : { value: [meanX, meanY] } + ) as T; + result.push(meanPoint); } - result.push(frame.min.point); - if (frame.min.x < frame.max.x) { - result.push(frame.max.point); + } else { + // Use min/max values for each frame + for (const [_i, framePoints] of frames) { + let minPoint = framePoints[0]; + let maxPoint = framePoints[0]; + + for (const p of framePoints) { + if (p.y < minPoint.y) { + minPoint = p; + } + if (p.y > maxPoint.y) { + maxPoint = p; + } + } + + // The order of the data must be preserved so max may be before min + if (minPoint.x > maxPoint.x) { + result.push(maxPoint.point); + } + result.push(minPoint.point); + if (minPoint.x < maxPoint.x) { + result.push(maxPoint.point); + } } } diff --git a/src/panels/lovelace/common/graph/coordinates.ts b/src/panels/lovelace/common/graph/coordinates.ts index 533bdd706b..73a1ffa43b 100644 --- a/src/panels/lovelace/common/graph/coordinates.ts +++ b/src/panels/lovelace/common/graph/coordinates.ts @@ -51,7 +51,8 @@ export const coordinates = ( width: number, height: number, maxDetails: number, - limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number } + limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number }, + useMean = false ) => { history = history.filter((item) => !Number.isNaN(item[1])); @@ -59,7 +60,8 @@ export const coordinates = ( history, maxDetails, limits?.minX, - limits?.maxX + limits?.maxX, + useMean ); return calcPoints(sampledData, width, height, limits); }; @@ -69,7 +71,8 @@ export const coordinatesMinimalResponseCompressedState = ( width: number, height: number, maxDetails: number, - limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number } + limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number }, + useMean = false ) => { if (!history?.length) { return { points: [], yAxisOrigin: 0 }; @@ -81,5 +84,5 @@ export const coordinatesMinimalResponseCompressedState = ( item.lu * 1000, Number(item.s), ]); - return coordinates(mappedHistory, width, height, maxDetails, limits); + return coordinates(mappedHistory, width, height, maxDetails, limits, useMean); }; diff --git a/src/panels/lovelace/header-footer/hui-graph-header-footer.ts b/src/panels/lovelace/header-footer/hui-graph-header-footer.ts index 9ce291f3d2..42125d7aec 100644 --- a/src/panels/lovelace/header-footer/hui-graph-header-footer.ts +++ b/src/panels/lovelace/header-footer/hui-graph-header-footer.ts @@ -155,16 +155,20 @@ export class HuiGraphHeaderFooter } const width = this.clientWidth || this.offsetWidth; // sample to 1 point per hour or 1 point per 5 pixels - const maxDetails = + const maxDetails = Math.max( + 10, this._config.detail! > 1 ? Math.max(width / 5, this._config.hours_to_show!) - : this._config.hours_to_show!; + : this._config.hours_to_show! + ); + const useMean = this._config.detail !== 2; const { points } = coordinatesMinimalResponseCompressedState( combinedHistory[this._config.entity], width, width / 5, maxDetails, - { minY: this._config.limits?.min, maxY: this._config.limits?.max } + { minY: this._config.limits?.min, maxY: this._config.limits?.max }, + useMean ); this._coordinates = points; }, From 19187f887dac79a9b92f287dbce091515ca3e3f6 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:23:16 +0100 Subject: [PATCH 60/66] Fix target picker for entity_id: none (#27893) Fix notFound condition to exclude 'none' in ha-target-picker-item-row --- src/components/target-picker/ha-target-picker-item-row.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/target-picker/ha-target-picker-item-row.ts b/src/components/target-picker/ha-target-picker-item-row.ts index 00782a3447..cfc07474b6 100644 --- a/src/components/target-picker/ha-target-picker-item-row.ts +++ b/src/components/target-picker/ha-target-picker-item-row.ts @@ -545,7 +545,7 @@ export class HaTargetPickerItemRow extends LitElement { name: entityName || deviceName || item, context, stateObject, - notFound: !stateObject && item !== "all", + notFound: !stateObject && item !== "all" && item !== "none", }; } From 10dc4324455d6549072b5581ca2e7425d053bb9f Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 10 Nov 2025 16:08:33 +0200 Subject: [PATCH 61/66] Fix entity name in statistics chart (#27896) --- src/panels/lovelace/cards/hui-statistics-graph-card.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/panels/lovelace/cards/hui-statistics-graph-card.ts b/src/panels/lovelace/cards/hui-statistics-graph-card.ts index 6c6c1b1fab..45cfb8ef3c 100644 --- a/src/panels/lovelace/cards/hui-statistics-graph-card.ts +++ b/src/panels/lovelace/cards/hui-statistics-graph-card.ts @@ -175,9 +175,9 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { this._names = {}; this._entities.forEach((config) => { const stateObj = this.hass!.states[config.entity]; - this._names[config.entity] = stateObj - ? computeLovelaceEntityName(this.hass!, stateObj, config.name) - : config.entity; + this._names[config.entity] = + computeLovelaceEntityName(this.hass!, stateObj, config.name) || + config.entity; }); } From be392be1e6fefbbd060a440869b8159aa1e13686 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 11 Nov 2025 21:07:10 +0200 Subject: [PATCH 62/66] Increase ZHA reconfiguration dialog width for details view (#27909) --- .../integration-panels/zha/dialog-zha-reconfigure-device.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-reconfigure-device.ts b/src/panels/config/integrations/integration-panels/zha/dialog-zha-reconfigure-device.ts index 8ff08ce5c2..8d71981604 100644 --- a/src/panels/config/integrations/integration-panels/zha/dialog-zha-reconfigure-device.ts +++ b/src/panels/config/integrations/integration-panels/zha/dialog-zha-reconfigure-device.ts @@ -426,6 +426,10 @@ class DialogZHAReconfigureDevice extends LitElement { return [ haStyleDialog, css` + ha-dialog { + --mdc-dialog-max-width: 800px; + } + .wrapper { display: grid; grid-template-columns: 3fr 1fr 2fr; From d8e8c9aa02ce8a3a4a667e6ab8d94f6da2b1572b Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Thu, 13 Nov 2025 05:43:27 -0800 Subject: [PATCH 63/66] Fix media image on dashboard-level background (#27934) --- src/panels/lovelace/views/hui-view-background.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/panels/lovelace/views/hui-view-background.ts b/src/panels/lovelace/views/hui-view-background.ts index 4fed2ee379..db05684fe4 100644 --- a/src/panels/lovelace/views/hui-view-background.ts +++ b/src/panels/lovelace/views/hui-view-background.ts @@ -109,6 +109,7 @@ export class HUIViewBackground extends LitElement { protected willUpdate(changedProperties: PropertyValues) { super.willUpdate(changedProperties); + let applyTheme = false; if (changedProperties.has("hass") && this.hass) { const oldHass = changedProperties.get("hass"); if ( @@ -116,16 +117,18 @@ export class HUIViewBackground extends LitElement { this.hass.themes !== oldHass.themes || this.hass.selectedTheme !== oldHass.selectedTheme ) { - this._applyTheme(); - return; + applyTheme = true; } } if (changedProperties.has("background")) { - this._applyTheme(); + applyTheme = true; this._fetchMedia(); } if (changedProperties.has("resolvedImage")) { + applyTheme = true; + } + if (applyTheme) { this._applyTheme(); } } From cdb6562de87468deb520a57043811238dc6c50e3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:42:16 +0200 Subject: [PATCH 64/66] Update dependency js-yaml to v4.1.1 [SECURITY] (#27955) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 81 ++++++++++++++++++++++++++++++---------------------- 2 files changed, 48 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index 56523a8601..7f2f7d8eb3 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "home-assistant-js-websocket": "9.5.0", "idb-keyval": "6.2.2", "intl-messageformat": "10.7.18", - "js-yaml": "4.1.0", + "js-yaml": "4.1.1", "leaflet": "1.9.4", "leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch", "leaflet.markercluster": "1.5.3", diff --git a/yarn.lock b/yarn.lock index 237662d5dc..cb4e09afad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6146,12 +6146,12 @@ __metadata: languageName: node linkType: hard -"baseline-browser-mapping@npm:^2.8.9": - version: 2.8.16 - resolution: "baseline-browser-mapping@npm:2.8.16" +"baseline-browser-mapping@npm:^2.8.19": + version: 2.8.21 + resolution: "baseline-browser-mapping@npm:2.8.21" bin: baseline-browser-mapping: dist/cli.js - checksum: 10/52a5807591daeffc810b783b1afa20c4017dd94e5bb74934bcde4dd408758e492610e330cfe6e609a0f0bde5ce210dd934271540fb931389d6838db17ec8cfef + checksum: 10/4154199589f9d5ca0cf80962494f34967e5bcbe2a3df234f4eb4b7a1766b263062ed47640686ab1d949f1156f5153bdc382ff7815368e365bc86916dc099d61b languageName: node linkType: hard @@ -6326,17 +6326,17 @@ __metadata: linkType: hard "browserslist@npm:^4.24.0, browserslist@npm:^4.25.1, browserslist@npm:^4.25.3": - version: 4.26.3 - resolution: "browserslist@npm:4.26.3" + version: 4.27.0 + resolution: "browserslist@npm:4.27.0" dependencies: - baseline-browser-mapping: "npm:^2.8.9" - caniuse-lite: "npm:^1.0.30001746" - electron-to-chromium: "npm:^1.5.227" - node-releases: "npm:^2.0.21" - update-browserslist-db: "npm:^1.1.3" + baseline-browser-mapping: "npm:^2.8.19" + caniuse-lite: "npm:^1.0.30001751" + electron-to-chromium: "npm:^1.5.238" + node-releases: "npm:^2.0.26" + update-browserslist-db: "npm:^1.1.4" bin: browserslist: cli.js - checksum: 10/49add06fd753a2514d84c75a7de8d9fb3d70be675e53b72981d87f0c0ff40d8a8cd0bd92f77400381704be0bf1c9c5c65aef95d03843d69475ff55188aa12124 + checksum: 10/56db4cdb98b5c93797a47e5a60decb144f73a2ae41c60a16c41b75516fabcb0db0116b8cfcf3a26c960cc6c9ab1c4f4801d8d3a743ec72f27acfe5380153ba2f languageName: node linkType: hard @@ -6480,10 +6480,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001746": - version: 1.0.30001750 - resolution: "caniuse-lite@npm:1.0.30001750" - checksum: 10/2b912758d817cd2c2c179246e282f8b598695ec733bc446183e1d381eada60889c4770a1dfd86075e046a43d55f9922e2eaed1501347fcb12a38716cc14be297 +"caniuse-lite@npm:^1.0.30001751": + version: 1.0.30001751 + resolution: "caniuse-lite@npm:1.0.30001751" + checksum: 10/608f7e1248b7023020382c7dbb0ef389693b3fc98193c3ccea2d44126306d6ac905a5061cf9e62bf640535a86e7a98e563b34c02f909296cfe228f41627a4dc7 languageName: node linkType: hard @@ -7455,10 +7455,10 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.5.227": - version: 1.5.235 - resolution: "electron-to-chromium@npm:1.5.235" - checksum: 10/fbc227d58a07dbb1b01e4a0f624a2fae03881f160a7c2e4416a68f30c83c1ca29b8f0e04056cb2851a6f493ebaf0d3b24bc2c7721d9e779cccbc9faeffef1c0e +"electron-to-chromium@npm:^1.5.238": + version: 1.5.243 + resolution: "electron-to-chromium@npm:1.5.243" + checksum: 10/cc1d566936aa05edcdef45c837bd3bf3c640b297f16d961d6b2b8536efb82bf1938a3dbc6d930e7561ffe4545c3d683dd6ffe57da37c1f6230defd32818785ab languageName: node linkType: hard @@ -9345,7 +9345,7 @@ __metadata: husky: "npm:9.1.7" idb-keyval: "npm:6.2.2" intl-messageformat: "npm:10.7.18" - js-yaml: "npm:4.1.0" + js-yaml: "npm:4.1.1" jsdom: "npm:27.0.1" jszip: "npm:3.10.1" leaflet: "npm:1.9.4" @@ -10415,14 +10415,14 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:4.1.0, js-yaml@npm:^4.1.0": - version: 4.1.0 - resolution: "js-yaml@npm:4.1.0" +"js-yaml@npm:4.1.1, js-yaml@npm:^4.1.0": + version: 4.1.1 + resolution: "js-yaml@npm:4.1.1" dependencies: argparse: "npm:^2.0.1" bin: js-yaml: bin/js-yaml.js - checksum: 10/c138a34a3fd0d08ebaf71273ad4465569a483b8a639e0b118ff65698d257c2791d3199e3f303631f2cb98213fa7b5f5d6a4621fd0fff819421b990d30d967140 + checksum: 10/a52d0519f0f4ef5b4adc1cde466cb54c50d56e2b4a983b9d5c9c0f2f99462047007a6274d7e95617a21d3c91fde3ee6115536ed70991cd645ba8521058b78f77 languageName: node linkType: hard @@ -11429,10 +11429,10 @@ __metadata: languageName: node linkType: hard -"node-releases@npm:^2.0.21": - version: 2.0.21 - resolution: "node-releases@npm:2.0.21" - checksum: 10/5344d634b39d20f47c0d85a1c64567fdb9cf46f7b27ed3d141f752642faab47dae326835c2109636f823758afb16ffbed7b0c0fe6f800ef91cec9f2beb4f2b4a +"node-releases@npm:^2.0.26": + version: 2.0.27 + resolution: "node-releases@npm:2.0.27" + checksum: 10/f6c78ddb392ae500719644afcbe68a9ea533242c02312eb6a34e8478506eb7482a3fb709c70235b01c32fe65625b68dfa9665113f816d87f163bc3819b62b106 languageName: node linkType: hard @@ -13843,7 +13843,7 @@ __metadata: languageName: node linkType: hard -"tar@npm:7.5.1, tar@npm:^7.4.3": +"tar@npm:7.5.1": version: 7.5.1 resolution: "tar@npm:7.5.1" dependencies: @@ -13856,6 +13856,19 @@ __metadata: languageName: node linkType: hard +"tar@npm:^7.4.3": + version: 7.5.2 + resolution: "tar@npm:7.5.2" + dependencies: + "@isaacs/fs-minipass": "npm:^4.0.0" + chownr: "npm:^3.0.0" + minipass: "npm:^7.1.2" + minizlib: "npm:^3.1.0" + yallist: "npm:^5.0.0" + checksum: 10/dbad9c9a07863cd1bdf8801d563b3280aa7dd0f4a6cead779ff7516d148dc80b4c04639ba732d47f91f04002f57e8c3c6573a717d649daecaac74ce71daa7ad3 + languageName: node + linkType: hard + "teex@npm:^1.0.1": version: 1.0.1 resolution: "teex@npm:1.0.1" @@ -14537,9 +14550,9 @@ __metadata: languageName: node linkType: hard -"update-browserslist-db@npm:^1.1.3": - version: 1.1.3 - resolution: "update-browserslist-db@npm:1.1.3" +"update-browserslist-db@npm:^1.1.4": + version: 1.1.4 + resolution: "update-browserslist-db@npm:1.1.4" dependencies: escalade: "npm:^3.2.0" picocolors: "npm:^1.1.1" @@ -14547,7 +14560,7 @@ __metadata: browserslist: ">= 4.21.0" bin: update-browserslist-db: cli.js - checksum: 10/87af2776054ffb9194cf95e0201547d041f72ee44ce54b144da110e65ea7ca01379367407ba21de5c9edd52c74d95395366790de67f3eb4cc4afa0fe4424e76f + checksum: 10/79b2c0a31e9b837b49dc55d5cb7b77f44a69502847c7be352a44b1d35ac2032bf0e1bb7543f992809ed427bf9d32aa3f7ad41cef96198fa959c1666870174c06 languageName: node linkType: hard From b11e787f09d88d7c7d3c29ba50f176615a320df0 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 21 Nov 2025 13:01:02 +0100 Subject: [PATCH 65/66] Dont add store token for external auth flows (#28026) * Dont add store token for external auth flows * Apply suggestion from @MindFreeze --------- Co-authored-by: Petar Petrov --- src/auth/ha-auth-flow.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/auth/ha-auth-flow.ts b/src/auth/ha-auth-flow.ts index 8125ff1d58..85446f199b 100644 --- a/src/auth/ha-auth-flow.ts +++ b/src/auth/ha-auth-flow.ts @@ -59,7 +59,8 @@ export class HaAuthFlow extends LitElement { willUpdate(changedProps: PropertyValues) { super.willUpdate(changedProps); - if (!this.hasUpdated) { + if (!this.hasUpdated && this.clientId === genClientId()) { + // Preselect store token when logging in to own instance this._storeToken = this.initStoreToken; } From ccc48d158a418b7446cd93de24c9e1d473e4e6af Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 21 Nov 2025 13:50:20 +0100 Subject: [PATCH 66/66] Bumped version to 20251105.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3a82f795c5..df05965ac3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20251105.0" +version = "20251105.1" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend"