From 2d549ba22fa27585fba47428ff1c24402e8b69d1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 22 May 2025 03:49:29 -0400 Subject: [PATCH 01/30] Voice wizard: Wait 10s for update entity to change state (#25556) --- .../voice-assistant-setup/voice-assistant-setup-step-update.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-update.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-update.ts index 999197139b..fc25a08ca3 100644 --- a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-update.ts +++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-update.ts @@ -131,7 +131,7 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement { ); this._refreshTimeout = window.setTimeout(() => { this._nextStep(); - }, 5000); + }, 10000); } else { this._nextStep(); } From 9b7db191a66a3e95c3f0a256f6f091005a6116c3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 22 May 2025 11:32:44 +0200 Subject: [PATCH 02/30] Add range support to icon translations (#25541) Co-authored-by: Bram Kragten --- src/common/entity/state_icon.ts | 8 --- src/data/icons.ts | 88 ++++++++++++++++++++++++++++++--- 2 files changed, 82 insertions(+), 14 deletions(-) diff --git a/src/common/entity/state_icon.ts b/src/common/entity/state_icon.ts index 58a63e881e..6eaef31725 100644 --- a/src/common/entity/state_icon.ts +++ b/src/common/entity/state_icon.ts @@ -2,7 +2,6 @@ import type { HassEntity } from "home-assistant-js-websocket"; import { computeStateDomain } from "./compute_state_domain"; import { updateIcon } from "./update_icon"; import { deviceTrackerIcon } from "./device_tracker_icon"; -import { batteryIcon } from "./battery_icon"; export const stateIcon = ( stateObj: HassEntity, @@ -10,17 +9,10 @@ export const stateIcon = ( ): string | undefined => { const domain = computeStateDomain(stateObj); const compareState = state ?? stateObj.state; - const dc = stateObj.attributes.device_class; switch (domain) { case "update": return updateIcon(stateObj, compareState); - case "sensor": - if (dc === "battery") { - return batteryIcon(stateObj, compareState); - } - break; - case "device_tracker": return deviceTrackerIcon(stateObj, compareState); diff --git a/src/data/icons.ts b/src/data/icons.ts index c59224b2a9..93cd21fee1 100644 --- a/src/data/icons.ts +++ b/src/data/icons.ts @@ -145,10 +145,12 @@ type PlatformIcons = Record< string, { state: Record; + range?: Record; state_attributes: Record< string, { state: Record; + range?: Record; default: string; } >; @@ -160,10 +162,12 @@ export type ComponentIcons = Record< string, { state?: Record; + range?: Record; state_attributes?: Record< string, { state: Record; + range?: Record; default: string; } >; @@ -286,6 +290,74 @@ export const getServiceIcons = async ( return resources.services.domains[domain]; }; +// Cache for sorted range keys +const sortedRangeCache = new WeakMap, number[]>(); + +// Helper function to get an icon from a range of values +const getIconFromRange = ( + value: number, + range: Record +): string | undefined => { + // Get cached range values or compute and cache them + let rangeValues = sortedRangeCache.get(range); + if (!rangeValues) { + rangeValues = Object.keys(range) + .map(Number) + .filter((k) => !isNaN(k)) + .sort((a, b) => a - b); + sortedRangeCache.set(range, rangeValues); + } + + if (rangeValues.length === 0) { + return undefined; + } + + // If the value is below the first threshold, return undefined + // (we'll fall back to the default icon) + if (value < rangeValues[0]) { + return undefined; + } + + // Find the highest threshold that's less than or equal to the value + let selectedThreshold = rangeValues[0]; + for (const threshold of rangeValues) { + if (value >= threshold) { + selectedThreshold = threshold; + } else { + break; + } + } + + return range[selectedThreshold.toString()]; +}; + +// Helper function to get an icon based on state and translations +const getIconFromTranslations = ( + state: string | number | undefined, + translations: + | { + default?: string; + state?: Record; + range?: Record; + } + | undefined +): string | undefined => { + if (!translations) { + return undefined; + } + + // First check for exact state match + if (state && translations.state?.[state]) { + return translations.state[state]; + } + // Then check for range-based icons if we have a numeric state + if (state !== undefined && translations.range && !isNaN(Number(state))) { + return getIconFromRange(Number(state), translations.range); + } + // Fallback to default icon + return translations.default; +}; + export const entityIcon = async ( hass: HomeAssistant, stateObj: HassEntity, @@ -331,7 +403,8 @@ const getEntityIcon = async ( const platformIcons = await getPlatformIcons(hass, platform); if (platformIcons) { const translations = platformIcons[domain]?.[translation_key]; - icon = (state && translations?.state?.[state]) || translations?.default; + + icon = getIconFromTranslations(state, translations); } } @@ -345,7 +418,8 @@ const getEntityIcon = async ( const translations = (device_class && entityComponentIcons[device_class]) || entityComponentIcons._; - icon = (state && translations?.state?.[state]) || translations?.default; + + icon = getIconFromTranslations(state, translations); } } return icon; @@ -372,9 +446,10 @@ export const attributeIcon = async ( if (translation_key && platform) { const platformIcons = await getPlatformIcons(hass, platform); if (platformIcons) { - const translations = - platformIcons[domain]?.[translation_key]?.state_attributes?.[attribute]; - icon = (value && translations?.state?.[value]) || translations?.default; + icon = getIconFromTranslations( + value, + platformIcons[domain]?.[translation_key]?.state_attributes?.[attribute] + ); } } if (!icon) { @@ -384,7 +459,8 @@ export const attributeIcon = async ( (deviceClass && entityComponentIcons[deviceClass]?.state_attributes?.[attribute]) || entityComponentIcons._?.state_attributes?.[attribute]; - icon = (value && translations?.state?.[value]) || translations?.default; + + icon = getIconFromTranslations(value, translations); } } return icon; From 3355986585dd09bb4e550cadbefe592103fdcb80 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 22 May 2025 11:51:12 +0200 Subject: [PATCH 03/30] Restore screen readers support on pickers (#25553) --- src/components/entity/ha-entity-picker.ts | 2 ++ src/components/entity/ha-statistic-picker.ts | 2 ++ src/components/ha-combo-box-textfield.ts | 24 +++++++++++++++ src/components/ha-combo-box.ts | 31 ++++++++++++++++---- src/components/ha-generic-picker.ts | 2 ++ src/components/ha-picker-combo-box.ts | 12 ++++---- 6 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 src/components/ha-combo-box-textfield.ts diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index 397685a32b..5968b4a103 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -317,6 +317,7 @@ export class HaEntityPicker extends LitElement { const secondary = [areaName, entityName ? deviceName : undefined] .filter(Boolean) .join(isRTL ? " ◂ " : " ▸ "); + const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - "); return { id: entityId, @@ -332,6 +333,7 @@ export class HaEntityPicker extends LitElement { friendlyName, entityId, ].filter(Boolean) as string[], + a11y_label: a11yLabel, stateObj: stateObj, }; }); diff --git a/src/components/entity/ha-statistic-picker.ts b/src/components/entity/ha-statistic-picker.ts index 3b6e262fc3..955071a8f2 100644 --- a/src/components/entity/ha-statistic-picker.ts +++ b/src/components/entity/ha-statistic-picker.ts @@ -267,6 +267,7 @@ export class HaStatisticPicker extends LitElement { const secondary = [areaName, entityName ? deviceName : undefined] .filter(Boolean) .join(isRTL ? " ◂ " : " ▸ "); + const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - "); const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`; output.push({ @@ -274,6 +275,7 @@ export class HaStatisticPicker extends LitElement { statistic_id: id, primary, secondary, + a11y_label: a11yLabel, stateObj: stateObj, type: "entity", sorting_label: [sortingPrefix, deviceName, entityName].join("_"), diff --git a/src/components/ha-combo-box-textfield.ts b/src/components/ha-combo-box-textfield.ts new file mode 100644 index 0000000000..19c87f268c --- /dev/null +++ b/src/components/ha-combo-box-textfield.ts @@ -0,0 +1,24 @@ +import type { PropertyValues } from "lit"; +import { customElement, property } from "lit/decorators"; +import { HaTextField } from "./ha-textfield"; + +@customElement("ha-combo-box-textfield") +export class HaComboBoxTextField extends HaTextField { + @property({ type: Boolean, attribute: "disable-set-value" }) + public disableSetValue = false; + + protected willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + if (changedProps.has("value")) { + if (this.disableSetValue) { + this.value = changedProps.get("value") as string; + } + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-combo-box-textfield": HaComboBoxTextField; + } +} diff --git a/src/components/ha-combo-box.ts b/src/components/ha-combo-box.ts index 9804ebe774..58ed2e1f51 100644 --- a/src/components/ha-combo-box.ts +++ b/src/components/ha-combo-box.ts @@ -12,11 +12,12 @@ import type { import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles"; import type { TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; -import { customElement, property, query } from "lit/decorators"; +import { customElement, property, query, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import { fireEvent } from "../common/dom/fire_event"; import type { HomeAssistant } from "../types"; import "./ha-combo-box-item"; +import "./ha-combo-box-textfield"; import "./ha-icon-button"; import "./ha-textfield"; import type { HaTextField } from "./ha-textfield"; @@ -108,9 +109,14 @@ export class HaComboBox extends LitElement { @property({ type: Boolean, attribute: "hide-clear-icon" }) public hideClearIcon = false; + @property({ type: Boolean, attribute: "clear-initial-value" }) + public clearInitialValue = false; + @query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight; - @query("ha-textfield", true) private _inputElement!: HaTextField; + @query("ha-combo-box-textfield", true) private _inputElement!: HaTextField; + + @state({ type: Boolean }) private _disableSetValue = false; private _overlayMutationObserver?: MutationObserver; @@ -171,7 +177,7 @@ export class HaComboBox extends LitElement { @value-changed=${this._valueChanged} attr-for-value="value" > - - + ${this.value && !this.hideClearIcon ? html` { + this._disableSetValue = false; + }, 100); + } else { + this._disableSetValue = true; + } + } + if (opened) { const overlay = document.querySelector( "vaadin-combo-box-overlay" @@ -342,10 +361,10 @@ export class HaComboBox extends LitElement { position: relative; --vaadin-combo-box-overlay-max-height: calc(45vh - 56px); } - ha-textfield { + ha-combo-box-textfield { width: 100%; } - ha-textfield > ha-icon-button { + ha-combo-box-textfield > ha-icon-button { --mdc-icon-button-size: 24px; padding: 2px; color: var(--secondary-text-color); diff --git a/src/components/ha-generic-picker.ts b/src/components/ha-generic-picker.ts index 38ba4487b1..2c699382fd 100644 --- a/src/components/ha-generic-picker.ts +++ b/src/components/ha-generic-picker.ts @@ -2,6 +2,7 @@ import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light"; import { css, html, LitElement, nothing, type CSSResultGroup } from "lit"; import { customElement, property, query, state } from "lit/decorators"; +import { ifDefined } from "lit/directives/if-defined"; import { fireEvent } from "../common/dom/fire_event"; import type { HomeAssistant } from "../types"; import "./ha-combo-box-item"; @@ -74,6 +75,7 @@ export class HaGenericPicker extends LitElement { ((item) => ({ ...item, - label: "", + a11y_label: item.a11y_label || item.primary, })); }; @@ -128,7 +129,7 @@ export class HaPickerComboBox extends LitElement { const sortedItems = items .map((item) => ({ ...item, - label: "", + a11y_label: item.a11y_label || item.primary, })) .sort((entityA, entityB) => caseInsensitiveStringCompare( @@ -175,7 +176,8 @@ export class HaPickerComboBox extends LitElement { Date: Thu, 22 May 2025 18:14:26 +0200 Subject: [PATCH 04/30] Improve category picker UI and search (#25560) --- src/components/ha-label-picker.ts | 37 +- .../config/category/ha-category-picker.ts | 351 +++++++++--------- 2 files changed, 190 insertions(+), 198 deletions(-) diff --git a/src/components/ha-label-picker.ts b/src/components/ha-label-picker.ts index a5360cc34a..4fdf3dc6f9 100644 --- a/src/components/ha-label-picker.ts +++ b/src/components/ha-label-picker.ts @@ -116,23 +116,26 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) { } ); - private _valueRenderer: PickerValueRenderer = (value) => { - const label = this._labelMap(this._labels).get(value); + private _computeValueRenderer = memoizeOne( + (labels: LabelRegistryEntry[] | undefined): PickerValueRenderer => + (value) => { + const label = this._labelMap(labels).get(value); - if (!label) { - return html` - - ${value} - `; - } + if (!label) { + return html` + + ${value} + `; + } - return html` - ${label.icon - ? html`` - : html``} - ${label.name} - `; - }; + return html` + ${label.icon + ? html`` + : html``} + ${label.name} + `; + } + ); private _getLabels = memoizeOne( ( @@ -388,6 +391,8 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) { this.placeholder ?? this.hass.localize("ui.components.label-picker.label"); + const valueRenderer = this._computeValueRenderer(this._labels); + return html` diff --git a/src/panels/config/category/ha-category-picker.ts b/src/panels/config/category/ha-category-picker.ts index 9d2247a7af..b4d987b3cb 100644 --- a/src/panels/config/category/ha-category-picker.ts +++ b/src/panels/config/category/ha-category-picker.ts @@ -1,17 +1,14 @@ -import { mdiTag } from "@mdi/js"; -import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; +import { mdiTag, mdiPlus } from "@mdi/js"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; -import type { PropertyValues } from "lit"; -import { html, LitElement, nothing } from "lit"; +import type { TemplateResult } from "lit"; +import { html, LitElement } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; -import type { ScorableTextItem } from "../../../common/string/filter/sequence-matching"; -import { fuzzyFilterSort } from "../../../common/string/filter/sequence-matching"; -import "../../../components/ha-combo-box"; -import type { HaComboBox } from "../../../components/ha-combo-box"; -import "../../../components/ha-combo-box-item"; -import "../../../components/ha-icon-button"; +import "../../../components/ha-generic-picker"; +import type { HaGenericPicker } from "../../../components/ha-generic-picker"; +import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box"; +import type { PickerValueRenderer } from "../../../components/ha-picker-field"; import "../../../components/ha-svg-icon"; import type { CategoryRegistryEntry } from "../../../data/category_registry"; import { @@ -22,20 +19,8 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import type { HomeAssistant, ValueChangedEvent } from "../../../types"; import { showCategoryRegistryDetailDialog } from "./show-dialog-category-registry-detail"; -type ScorableCategoryRegistryEntry = ScorableTextItem & CategoryRegistryEntry; - const ADD_NEW_ID = "___ADD_NEW___"; const NO_CATEGORIES_ID = "___NO_CATEGORIES___"; -const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___"; - -const rowRenderer: ComboBoxLitRenderer = (item) => html` - - ${item.icon - ? html`` - : html``} - ${item.name} - -`; @customElement("ha-category-picker") export class HaCategoryPicker extends SubscribeMixin(LitElement) { @@ -58,14 +43,17 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) { @property({ type: Boolean }) public required = false; - @state() private _opened?: boolean; - @state() private _categories?: CategoryRegistryEntry[]; - @query("ha-combo-box", true) public comboBox!: HaComboBox; + @query("ha-generic-picker") private _picker?: HaGenericPicker; protected hassSubscribeRequiredHostProps = ["scope"]; + public async open() { + await this.updateComplete; + await this._picker?.open(); + } + protected hassSubscribe(): (UnsubscribeFunc | Promise)[] { return [ subscribeCategoryRegistry( @@ -78,186 +66,185 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) { ]; } - private _suggestion?: string; - - private _init = false; - - public async open() { - await this.updateComplete; - await this.comboBox?.open(); - } - - public async focus() { - await this.updateComplete; - await this.comboBox?.focus(); - } - - private _getCategories = memoizeOne( + private _categoryMap = memoizeOne( ( - categories: CategoryRegistryEntry[] | undefined, - noAdd: this["noAdd"] - ): CategoryRegistryEntry[] => { - const result = categories ? [...categories] : []; - if (!result?.length) { - result.push({ - category_id: NO_CATEGORIES_ID, - name: this.hass.localize( - "ui.components.category-picker.no_categories" - ), - icon: null, - }); + categories: CategoryRegistryEntry[] | undefined + ): Map => { + if (!categories) { + return new Map(); } - - return noAdd - ? result - : [ - ...result, - { - category_id: ADD_NEW_ID, - name: this.hass.localize("ui.components.category-picker.add_new"), - icon: "mdi:plus", - }, - ]; + return new Map( + categories.map((category) => [category.category_id, category]) + ); } ); - protected updated(changedProps: PropertyValues) { - if ( - (!this._init && this.hass && this._categories) || - (this._init && changedProps.has("_opened") && this._opened) - ) { - this._init = true; - const categories = this._getCategories(this._categories, this.noAdd).map( - (label) => ({ - ...label, - strings: [label.name], - }) - ); - this.comboBox.items = categories; - this.comboBox.filteredItems = categories; - } - } + private _computeValueRenderer = memoizeOne( + (categories: CategoryRegistryEntry[] | undefined): PickerValueRenderer => + (value) => { + const category = this._categoryMap(categories).get(value); - protected render() { - if (!this._categories) { - return nothing; - } - return html` - - - `; - } + if (!category) { + return html` + + ${value} + `; + } - private _filterChanged(ev: CustomEvent): void { - const target = ev.target as HaComboBox; - const filterString = ev.detail.value; - if (!filterString) { - this.comboBox.filteredItems = this.comboBox.items; - return; - } + return html` + ${category.icon + ? html`` + : html``} + ${category.name} + `; + } + ); - const filteredItems = fuzzyFilterSort( - filterString, - target.items?.filter( - (item) => ![NO_CATEGORIES_ID, ADD_NEW_ID].includes(item.category_id) - ) || [] - ); - if (filteredItems?.length === 0) { - if (this.noAdd) { - this.comboBox.filteredItems = [ + private _getCategories = memoizeOne( + (categories: CategoryRegistryEntry[] | undefined): PickerComboBoxItem[] => { + if (!categories || categories.length === 0) { + return [ { - category_id: NO_CATEGORIES_ID, - name: this.hass.localize("ui.components.category-picker.no_match"), - icon: null, - }, - ] as ScorableCategoryRegistryEntry[]; - } else { - this._suggestion = filterString; - this.comboBox.filteredItems = [ - { - category_id: ADD_NEW_SUGGESTION_ID, - name: this.hass.localize( - "ui.components.category-picker.add_new_sugestion", - { name: this._suggestion } + id: NO_CATEGORIES_ID, + primary: this.hass.localize( + "ui.components.category-picker.no_categories" ), - icon: "mdi:plus", + icon_path: mdiTag, }, ]; } - } else { - this.comboBox.filteredItems = filteredItems; + + const items = categories.map((category) => ({ + id: category.category_id, + primary: category.name, + icon: category.icon || undefined, + icon_path: category.icon ? undefined : mdiTag, + sorting_label: category.name, + search_labels: [category.name, category.category_id].filter( + (v): v is string => Boolean(v) + ), + })); + + return items; } - } + ); - private get _value() { - return this.value || ""; - } + private _getItems = () => this._getCategories(this._categories); - private _openedChanged(ev: ValueChangedEvent) { - this._opened = ev.detail.value; - } - - private _categoryChanged(ev: ValueChangedEvent) { - ev.stopPropagation(); - let newValue = ev.detail.value; - - if (newValue === NO_CATEGORIES_ID) { - newValue = ""; - this.comboBox.setInputValue(""); - return; - } - - if (![ADD_NEW_SUGGESTION_ID, ADD_NEW_ID].includes(newValue)) { - if (newValue !== this._value) { - this._setValue(newValue); + private _allCategoryNames = memoizeOne( + (categories?: CategoryRegistryEntry[]) => { + if (!categories) { + return []; } + return [ + ...new Set( + categories + .map((category) => category.name.toLowerCase()) + .filter(Boolean) as string[] + ), + ]; + } + ); + + private _getAdditionalItems = ( + searchString?: string + ): PickerComboBoxItem[] => { + if (this.noAdd) { + return []; + } + + const allCategoryNames = this._allCategoryNames(this._categories); + + if ( + searchString && + !allCategoryNames.includes(searchString.toLowerCase()) + ) { + return [ + { + id: ADD_NEW_ID + searchString, + primary: this.hass.localize( + "ui.components.category-picker.add_new_sugestion", + { + name: searchString, + } + ), + icon_path: mdiPlus, + }, + ]; + } + + return [ + { + id: ADD_NEW_ID, + primary: this.hass.localize("ui.components.category-picker.add_new"), + icon_path: mdiPlus, + }, + ]; + }; + + protected render(): TemplateResult { + const placeholder = + this.placeholder ?? + this.hass.localize("ui.components.category-picker.category"); + + const valueRenderer = this._computeValueRenderer(this._categories); + + return html` + + + `; + } + + private _valueChanged(ev: ValueChangedEvent) { + ev.stopPropagation(); + + const value = ev.detail.value; + + if (value === NO_CATEGORIES_ID) { return; } - (ev.target as any).value = this._value; + if (!value) { + this._setValue(undefined); + return; + } - this.hass.loadFragmentTranslation("config"); + if (value.startsWith(ADD_NEW_ID)) { + this.hass.loadFragmentTranslation("config"); - showCategoryRegistryDetailDialog(this, { - scope: this.scope!, - suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "", - createEntry: async (values) => { - const category = await createCategoryRegistryEntry( - this.hass, - this.scope!, - values - ); - this._categories = [...this._categories!, category]; - this.comboBox.filteredItems = this._getCategories( - this._categories, - this.noAdd - ); - await this.updateComplete; - await this.comboBox.updateComplete; - this._setValue(category.category_id); - return category; - }, - }); + const suggestedName = value.substring(ADD_NEW_ID.length); - this._suggestion = undefined; - this.comboBox.setInputValue(""); + showCategoryRegistryDetailDialog(this, { + scope: this.scope!, + suggestedName: suggestedName, + createEntry: async (values) => { + const category = await createCategoryRegistryEntry( + this.hass, + this.scope!, + values + ); + this._setValue(category.category_id); + return category; + }, + }); + + return; + } + + this._setValue(value); } private _setValue(value?: string) { From ec26818c5352fe5038a22798ca36b91809344e1d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 18:26:19 +0200 Subject: [PATCH 05/30] Update dependency @types/leaflet to v1.9.18 (#25568) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 248a54eb3c..46a5a97235 100644 --- a/package.json +++ b/package.json @@ -168,7 +168,7 @@ "@types/glob": "8.1.0", "@types/html-minifier-terser": "7.0.2", "@types/js-yaml": "4.0.9", - "@types/leaflet": "1.9.17", + "@types/leaflet": "1.9.18", "@types/leaflet-draw": "1.0.12", "@types/leaflet.markercluster": "1.5.5", "@types/lodash.merge": "4.6.9", diff --git a/yarn.lock b/yarn.lock index d4ddbf9817..316ed56325 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4587,12 +4587,12 @@ __metadata: languageName: node linkType: hard -"@types/leaflet@npm:*, @types/leaflet@npm:1.9.17": - version: 1.9.17 - resolution: "@types/leaflet@npm:1.9.17" +"@types/leaflet@npm:*, @types/leaflet@npm:1.9.18": + version: 1.9.18 + resolution: "@types/leaflet@npm:1.9.18" dependencies: "@types/geojson": "npm:*" - checksum: 10/6520b42c98a31340f49e456cc8e4c1a2a73f4ff262ca7293fbd9aa931fc34dd61699a70e9c67110773092bd6c397911918690e339b0c3fa1bc471ef21fd55932 + checksum: 10/33085a5377e3c5656b9c2d5e17e2a7530363fbb1bb5825d2e959945c33d9f4579efca8eed9299cbe60609d19be2debbb8b14782197ac5466a86fb83b903a5e8d languageName: node linkType: hard @@ -9301,7 +9301,7 @@ __metadata: "@types/glob": "npm:8.1.0" "@types/html-minifier-terser": "npm:7.0.2" "@types/js-yaml": "npm:4.0.9" - "@types/leaflet": "npm:1.9.17" + "@types/leaflet": "npm:1.9.18" "@types/leaflet-draw": "npm:1.0.12" "@types/leaflet.markercluster": "npm:1.5.5" "@types/lodash.merge": "npm:4.6.9" From 5581c10139a1f2c9db450cfd516cc1393a787ce5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 16:36:02 +0000 Subject: [PATCH 06/30] Update vitest monorepo to v3.1.4 (#25569) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 +- yarn.lock | 120 +++++++++++++++++++++++++-------------------------- 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/package.json b/package.json index 46a5a97235..30ed365ccb 100644 --- a/package.json +++ b/package.json @@ -179,7 +179,7 @@ "@types/tar": "6.1.13", "@types/ua-parser-js": "0.7.39", "@types/webspeechapi": "0.0.29", - "@vitest/coverage-v8": "3.1.3", + "@vitest/coverage-v8": "3.1.4", "babel-loader": "10.0.0", "babel-plugin-template-html-minifier": "4.1.0", "browserslist-useragent-regexp": "4.1.3", @@ -220,7 +220,7 @@ "typescript": "5.8.3", "typescript-eslint": "8.32.1", "vite-tsconfig-paths": "5.1.4", - "vitest": "3.1.3", + "vitest": "3.1.4", "webpack-stats-plugin": "1.1.3", "webpackbar": "7.0.0", "workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch" diff --git a/yarn.lock b/yarn.lock index 316ed56325..6f19bcdd05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5252,9 +5252,9 @@ __metadata: languageName: node linkType: hard -"@vitest/coverage-v8@npm:3.1.3": - version: 3.1.3 - resolution: "@vitest/coverage-v8@npm:3.1.3" +"@vitest/coverage-v8@npm:3.1.4": + version: 3.1.4 + resolution: "@vitest/coverage-v8@npm:3.1.4" dependencies: "@ampproject/remapping": "npm:^2.3.0" "@bcoe/v8-coverage": "npm:^1.0.2" @@ -5269,32 +5269,32 @@ __metadata: test-exclude: "npm:^7.0.1" tinyrainbow: "npm:^2.0.0" peerDependencies: - "@vitest/browser": 3.1.3 - vitest: 3.1.3 + "@vitest/browser": 3.1.4 + vitest: 3.1.4 peerDependenciesMeta: "@vitest/browser": optional: true - checksum: 10/2f942cc67dfc5a4c3eb6661020959c52893724bb1ef8d47ba82a05b787fb6ba325e2489910f2c568fa33701bb6f1868d28fd0c30b80ce54cd938f491ff8cc3f5 + checksum: 10/93680edce574bf698b32d2c2e3f6804384a097c6d32c89edcea1468ee735fb18b43005b27ab68b43701e48d87783e22d1bde835e6f6882a2fa46765a26e24224 languageName: node linkType: hard -"@vitest/expect@npm:3.1.3": - version: 3.1.3 - resolution: "@vitest/expect@npm:3.1.3" +"@vitest/expect@npm:3.1.4": + version: 3.1.4 + resolution: "@vitest/expect@npm:3.1.4" dependencies: - "@vitest/spy": "npm:3.1.3" - "@vitest/utils": "npm:3.1.3" + "@vitest/spy": "npm:3.1.4" + "@vitest/utils": "npm:3.1.4" chai: "npm:^5.2.0" tinyrainbow: "npm:^2.0.0" - checksum: 10/f63053849430e93e85cd50994a75f32e6b73d35fefbf7894f1869c356ed6c601adfc95c66004b2df3c49335300202286480c47d841d78d2047af6bee00f8b3ed + checksum: 10/2d438562fd75ee64f0506a785f9825962f765889e63179e6d64cad338ff8fb0466bafaec9e94e6dea814ebf7287209f605ce49e4cf487610d98ccba61fee061b languageName: node linkType: hard -"@vitest/mocker@npm:3.1.3": - version: 3.1.3 - resolution: "@vitest/mocker@npm:3.1.3" +"@vitest/mocker@npm:3.1.4": + version: 3.1.4 + resolution: "@vitest/mocker@npm:3.1.4" dependencies: - "@vitest/spy": "npm:3.1.3" + "@vitest/spy": "npm:3.1.4" estree-walker: "npm:^3.0.3" magic-string: "npm:^0.30.17" peerDependencies: @@ -5305,57 +5305,57 @@ __metadata: optional: true vite: optional: true - checksum: 10/fc4a8ee015551f476af56ee27327c78fd6f8a023eea79a92834482d10272c74dd0a39631b2d55341e54ac04803b1d2710527b34ed206ede18cde9706a1582ed8 + checksum: 10/1e50441da229ea4999aa686669fda1cc03bcfd93162a42f7660b6e5897b6e6e2e31f54a2028c6d5510fda552ff7c27cef88fecb7efee937df28a670e59c36ca4 languageName: node linkType: hard -"@vitest/pretty-format@npm:3.1.3, @vitest/pretty-format@npm:^3.1.3": - version: 3.1.3 - resolution: "@vitest/pretty-format@npm:3.1.3" +"@vitest/pretty-format@npm:3.1.4, @vitest/pretty-format@npm:^3.1.4": + version: 3.1.4 + resolution: "@vitest/pretty-format@npm:3.1.4" dependencies: tinyrainbow: "npm:^2.0.0" - checksum: 10/da508750f47b4043e9aaea803f37dada4d3121b63a8fd2a7c77849a380d9040ca488291f6ee98e7ee3e6543bd6c2ed7cdad99b6b86897999c740462ef617413a + checksum: 10/d8c831410d2cc755d899f31a5f7298ad336f4cddc3115d7da5174595098144a3282eee89a54fb05c6592d408bf4a86e66fa5636c9304816a6557b833d0f98748 languageName: node linkType: hard -"@vitest/runner@npm:3.1.3": - version: 3.1.3 - resolution: "@vitest/runner@npm:3.1.3" +"@vitest/runner@npm:3.1.4": + version: 3.1.4 + resolution: "@vitest/runner@npm:3.1.4" dependencies: - "@vitest/utils": "npm:3.1.3" + "@vitest/utils": "npm:3.1.4" pathe: "npm:^2.0.3" - checksum: 10/7862077b7663200801cd7903b977b3713a291f91b2b0930ee59951bec0ae51d38219308e543b62ff5eaed9ead51bcbd7175b19f9b7c0d876e2975defee76fdee + checksum: 10/45307642d00f28cbd9f196d55238aeac6d2024de9503a66c120981a0acfa43dcb06a00fbf7f06388f26c8bd5e1ed70fa59514e1644f7ec2f4c770f67666e3c0e languageName: node linkType: hard -"@vitest/snapshot@npm:3.1.3": - version: 3.1.3 - resolution: "@vitest/snapshot@npm:3.1.3" +"@vitest/snapshot@npm:3.1.4": + version: 3.1.4 + resolution: "@vitest/snapshot@npm:3.1.4" dependencies: - "@vitest/pretty-format": "npm:3.1.3" + "@vitest/pretty-format": "npm:3.1.4" magic-string: "npm:^0.30.17" pathe: "npm:^2.0.3" - checksum: 10/5889414ecd19df6a1cc09c57fc96d344721f01e5812d9153565208c76dac4d42fc1c636153b9701d50a1d5acd4fd8ce81c09c9592d97728a700c5a8af790d0a4 + checksum: 10/f307f7a7572a76c20287efb474543021751107e41f069c34f9a90be8d9196ead3182ca41fb0a5f2879c753e341727ab6cbbb3a7cbb1fd7551cb110458359b475 languageName: node linkType: hard -"@vitest/spy@npm:3.1.3": - version: 3.1.3 - resolution: "@vitest/spy@npm:3.1.3" +"@vitest/spy@npm:3.1.4": + version: 3.1.4 + resolution: "@vitest/spy@npm:3.1.4" dependencies: tinyspy: "npm:^3.0.2" - checksum: 10/9b42e219b40fde935e5bd7fa19ee99f01fc27ecd89a5fccabbbbc91e02eef3bd0530ba3769c2ff380529f708eb535a30cce773d680c708209a994c54d1d992fe + checksum: 10/e883766dbe8f07f371cc434e10bf50b66d2a31eab37bb9e12ad93b5a1e7e753543cdf2fbee9c0168c574cb6e9f8001871bc9dee45721cbeb370cabad1b8d08a5 languageName: node linkType: hard -"@vitest/utils@npm:3.1.3": - version: 3.1.3 - resolution: "@vitest/utils@npm:3.1.3" +"@vitest/utils@npm:3.1.4": + version: 3.1.4 + resolution: "@vitest/utils@npm:3.1.4" dependencies: - "@vitest/pretty-format": "npm:3.1.3" + "@vitest/pretty-format": "npm:3.1.4" loupe: "npm:^3.1.3" tinyrainbow: "npm:^2.0.0" - checksum: 10/d9971948161364e61e0fb08a053b9768f02054686f0a74e5b7bdc9c726271842d5f8c4256c68cf9aad2b83a28d2333c5694e336715d145e194fa1a93e64e97c3 + checksum: 10/221d9d7dfc41e1c16521e4d998e2980b4a731b38172ba103eb70489eaaff149d479108a21a6f79118885ca2c10e51fbcae5a24e00f7459139dbfbcec39171b10 languageName: node linkType: hard @@ -9315,7 +9315,7 @@ __metadata: "@vaadin/combo-box": "npm:24.7.6" "@vaadin/vaadin-themable-mixin": "npm:24.7.6" "@vibrant/color": "npm:4.0.0" - "@vitest/coverage-v8": "npm:3.1.3" + "@vitest/coverage-v8": "npm:3.1.4" "@vue/web-component-wrapper": "npm:1.3.0" "@webcomponents/scoped-custom-element-registry": "npm:0.0.10" "@webcomponents/webcomponentsjs": "npm:2.8.0" @@ -9401,7 +9401,7 @@ __metadata: ua-parser-js: "npm:2.0.3" vis-data: "npm:7.1.9" vite-tsconfig-paths: "npm:5.1.4" - vitest: "npm:3.1.3" + vitest: "npm:3.1.4" vue: "npm:2.7.16" vue2-daterange-picker: "npm:0.6.8" webpack-stats-plugin: "npm:1.1.3" @@ -14857,9 +14857,9 @@ __metadata: languageName: node linkType: hard -"vite-node@npm:3.1.3": - version: 3.1.3 - resolution: "vite-node@npm:3.1.3" +"vite-node@npm:3.1.4": + version: 3.1.4 + resolution: "vite-node@npm:3.1.4" dependencies: cac: "npm:^6.7.14" debug: "npm:^4.4.0" @@ -14868,7 +14868,7 @@ __metadata: vite: "npm:^5.0.0 || ^6.0.0" bin: vite-node: vite-node.mjs - checksum: 10/59c1e1397b055861390cf4e540ba1e968e4ad140df8e214f797dd73b9130f00855712779d4f6f0c8c5149bfe95db20ad55f349dd1962a143117a0d71d956235f + checksum: 10/e4c198fe9447b4182b95601249a1e8be183380bbd2875034e8ed741a67895b8ad5c6324d6a6db7d60c2b0372f5209ca046604abfc0a70c04bffef00e5a4f6e19 languageName: node linkType: hard @@ -14943,17 +14943,17 @@ __metadata: languageName: node linkType: hard -"vitest@npm:3.1.3": - version: 3.1.3 - resolution: "vitest@npm:3.1.3" +"vitest@npm:3.1.4": + version: 3.1.4 + resolution: "vitest@npm:3.1.4" dependencies: - "@vitest/expect": "npm:3.1.3" - "@vitest/mocker": "npm:3.1.3" - "@vitest/pretty-format": "npm:^3.1.3" - "@vitest/runner": "npm:3.1.3" - "@vitest/snapshot": "npm:3.1.3" - "@vitest/spy": "npm:3.1.3" - "@vitest/utils": "npm:3.1.3" + "@vitest/expect": "npm:3.1.4" + "@vitest/mocker": "npm:3.1.4" + "@vitest/pretty-format": "npm:^3.1.4" + "@vitest/runner": "npm:3.1.4" + "@vitest/snapshot": "npm:3.1.4" + "@vitest/spy": "npm:3.1.4" + "@vitest/utils": "npm:3.1.4" chai: "npm:^5.2.0" debug: "npm:^4.4.0" expect-type: "npm:^1.2.1" @@ -14966,14 +14966,14 @@ __metadata: tinypool: "npm:^1.0.2" tinyrainbow: "npm:^2.0.0" vite: "npm:^5.0.0 || ^6.0.0" - vite-node: "npm:3.1.3" + vite-node: "npm:3.1.4" why-is-node-running: "npm:^2.3.0" peerDependencies: "@edge-runtime/vm": "*" "@types/debug": ^4.1.12 "@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0 - "@vitest/browser": 3.1.3 - "@vitest/ui": 3.1.3 + "@vitest/browser": 3.1.4 + "@vitest/ui": 3.1.4 happy-dom: "*" jsdom: "*" peerDependenciesMeta: @@ -14993,7 +14993,7 @@ __metadata: optional: true bin: vitest: vitest.mjs - checksum: 10/ae74b401b15847615ec664260cf83eb2ce67c4bf018228bd0c48eae2e94309104a8a49b42ef422c27905e438d367207da15364d500f72cf2b723aff448c6a4e6 + checksum: 10/e30f8df59d3e551c9a104dcf1e9937a0b1c3731072bcfe054a17124689852046b5c44bca0316b6ece0b301225f904709e2b990e8122d5bc7d08327d78785d6ac languageName: node linkType: hard From 67dc830bbf9290ade4c343528c75c23129c244e4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 08:52:22 +0300 Subject: [PATCH 07/30] Update dependency marked to v15.0.12 (#25571) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 30ed365ccb..eba1a08ffa 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ "lit": "3.3.0", "lit-html": "3.3.0", "luxon": "3.6.1", - "marked": "15.0.11", + "marked": "15.0.12", "memoize-one": "6.0.0", "node-vibrant": "4.0.3", "object-hash": "3.0.0", diff --git a/yarn.lock b/yarn.lock index 6f19bcdd05..e617aff51d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9375,7 +9375,7 @@ __metadata: lodash.template: "npm:4.5.0" luxon: "npm:3.6.1" map-stream: "npm:0.0.7" - marked: "npm:15.0.11" + marked: "npm:15.0.12" memoize-one: "npm:6.0.0" node-vibrant: "npm:4.0.3" object-hash: "npm:3.0.0" @@ -11065,12 +11065,12 @@ __metadata: languageName: node linkType: hard -"marked@npm:15.0.11": - version: 15.0.11 - resolution: "marked@npm:15.0.11" +"marked@npm:15.0.12": + version: 15.0.12 + resolution: "marked@npm:15.0.12" bin: marked: bin/marked.js - checksum: 10/939e75f3e989ef4d72d6da9c7e80e43d49ffdc7af8ae2a38cc8c73f20a629d9659a0acda1d0cb5a5f90876cbfb1520d029f98790a934b46592dfa178fbd2838c + checksum: 10/deeb619405c0c46af00c99b18b3365450abeb309104b24e3658f46142344f6b7c4117608c3b5834084d8738e92f81240c19f596e6ee369260f96e52b3457eaee languageName: node linkType: hard From 412a0e9f6aa946f06e9e575b71221e59fd892b30 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 23 May 2025 10:35:47 +0200 Subject: [PATCH 08/30] Improve area floor picker UI and search (#25540) * Improve area floor picker UI and search * Improve area floor picker UI and search * Remove noResultSorting --- src/components/ha-area-floor-picker.ts | 428 +++++++++++++------------ src/components/ha-picker-combo-box.ts | 2 +- src/components/ha-target-picker.ts | 6 +- 3 files changed, 223 insertions(+), 213 deletions(-) diff --git a/src/components/ha-area-floor-picker.ts b/src/components/ha-area-floor-picker.ts index ed52b1c9ee..1b2f4afb91 100644 --- a/src/components/ha-area-floor-picker.ts +++ b/src/components/ha-area-floor-picker.ts @@ -1,16 +1,16 @@ import { mdiTextureBox } from "@mdi/js"; import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import type { HassEntity } from "home-assistant-js-websocket"; -import type { PropertyValues, TemplateResult } from "lit"; +import type { TemplateResult } from "lit"; import { LitElement, html, nothing } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; +import { customElement, property, query } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; import { fireEvent } from "../common/dom/fire_event"; +import { computeAreaName } from "../common/entity/compute_area_name"; import { computeDomain } from "../common/entity/compute_domain"; +import { computeFloorName } from "../common/entity/compute_floor_name"; import { stringCompare } from "../common/string/compare"; -import type { ScorableTextItem } from "../common/string/filter/sequence-matching"; -import { fuzzyFilterSort } from "../common/string/filter/sequence-matching"; import { computeRTL } from "../common/util/compute_rtl"; import type { AreaRegistryEntry } from "../data/area_registry"; import type { @@ -19,29 +19,33 @@ import type { } from "../data/device_registry"; import { getDeviceEntityDisplayLookup } from "../data/device_registry"; import type { EntityRegistryDisplayEntry } from "../data/entity_registry"; -import type { FloorRegistryEntry } from "../data/floor_registry"; -import { getFloorAreaLookup } from "../data/floor_registry"; +import { + getFloorAreaLookup, + type FloorRegistryEntry, +} from "../data/floor_registry"; import type { HomeAssistant, ValueChangedEvent } from "../types"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; -import "./ha-combo-box"; -import type { HaComboBox } from "./ha-combo-box"; import "./ha-combo-box-item"; import "./ha-floor-icon"; +import "./ha-generic-picker"; +import type { HaGenericPicker } from "./ha-generic-picker"; import "./ha-icon-button"; +import type { PickerComboBoxItem } from "./ha-picker-combo-box"; +import type { PickerValueRenderer } from "./ha-picker-field"; import "./ha-svg-icon"; import "./ha-tree-indicator"; -type ScorableAreaFloorEntry = ScorableTextItem & FloorAreaEntry; +const SEPARATOR = "________"; -interface FloorAreaEntry { - id: string | null; - name: string; - icon: string | null; - strings: string[]; +interface FloorComboBoxItem extends PickerComboBoxItem { + type: "floor" | "area"; + floor?: FloorRegistryEntry; + area?: AreaRegistryEntry; +} + +interface AreaFloorValue { + id: string; type: "floor" | "area"; - level: number | null; - hasFloor?: boolean; - lastArea?: boolean; } @customElement("ha-area-floor-picker") @@ -50,12 +54,15 @@ export class HaAreaFloorPicker extends LitElement { @property() public label?: string; - @property() public value?: string; + @property({ attribute: false }) public value?: AreaFloorValue; @property() public helper?: string; @property() public placeholder?: string; + @property({ type: String, attribute: "search-label" }) + public searchLabel?: string; + /** * Show only areas with entities from specific domains. * @type {Array} @@ -106,66 +113,53 @@ export class HaAreaFloorPicker extends LitElement { @property({ type: Boolean }) public required = false; - @state() private _opened?: boolean; - - @query("ha-combo-box", true) public comboBox!: HaComboBox; - - private _init = false; + @query("ha-generic-picker") private _picker?: HaGenericPicker; public async open() { await this.updateComplete; - await this.comboBox?.open(); + await this._picker?.open(); } - public async focus() { - await this.updateComplete; - await this.comboBox?.focus(); - } + private _valueRenderer: PickerValueRenderer = (value: string) => { + const item = this._parseValue(value); + + const area = item.type === "area" && this.hass.areas[value]; + + if (area) { + const areaName = computeAreaName(area); + return html` + ${area.icon + ? html`` + : html``} + ${areaName} + `; + } + + const floor = item.type === "floor" && this.hass.floors[value]; + + if (floor) { + const floorName = computeFloorName(floor); + return html` + + ${floorName} + `; + } - private _rowRenderer: ComboBoxLitRenderer = (item) => { - const rtl = computeRTL(this.hass); return html` - - ${item.type === "area" && item.hasFloor - ? html` - - ` - : nothing} - ${item.type === "floor" - ? html`` - : item.icon - ? html`` - : html``} - ${item.name} - + + ${value} `; }; - private _getAreas = memoizeOne( + private _getAreasAndFloors = memoizeOne( ( - floors: FloorRegistryEntry[], - areas: AreaRegistryEntry[], - devices: DeviceRegistryEntry[], - entities: EntityRegistryDisplayEntry[], + haFloors: HomeAssistant["floors"], + haAreas: HomeAssistant["areas"], + haDevices: HomeAssistant["devices"], + haEntities: HomeAssistant["entities"], includeDomains: this["includeDomains"], excludeDomains: this["excludeDomains"], includeDeviceClasses: this["includeDeviceClasses"], @@ -173,19 +167,11 @@ export class HaAreaFloorPicker extends LitElement { entityFilter: this["entityFilter"], excludeAreas: this["excludeAreas"], excludeFloors: this["excludeFloors"] - ): FloorAreaEntry[] => { - if (!areas.length && !floors.length) { - return [ - { - id: "no_areas", - type: "area", - name: this.hass.localize("ui.components.area-picker.no_areas"), - icon: null, - strings: [], - level: null, - }, - ]; - } + ): FloorComboBoxItem[] => { + const floors = Object.values(haFloors); + const areas = Object.values(haAreas); + const devices = Object.values(haDevices); + const entities = Object.values(haEntities); let deviceEntityLookup: DeviceEntityDisplayLookup = {}; let inputDevices: DeviceRegistryEntry[] | undefined; @@ -326,19 +312,6 @@ export class HaAreaFloorPicker extends LitElement { ); } - if (!outputAreas.length) { - return [ - { - id: "no_areas", - type: "area", - name: this.hass.localize("ui.components.area-picker.no_match"), - icon: null, - strings: [], - level: null, - }, - ]; - } - const floorAreaLookup = getFloorAreaLookup(outputAreas); const unassisgnedAreas = Object.values(outputAreas).filter( (area) => !area.floor_id || !floorAreaLookup[area.floor_id] @@ -360,151 +333,186 @@ export class HaAreaFloorPicker extends LitElement { return stringCompare(floorA.name, floorB.name); }); - const output: FloorAreaEntry[] = []; + const items: FloorComboBoxItem[] = []; floorAreaEntries.forEach(([floor, floorAreas]) => { if (floor) { - output.push({ - id: floor.floor_id, + const floorName = computeFloorName(floor); + + const areaSearchLabels = floorAreas + .map((area) => { + const areaName = computeAreaName(area) || area.area_id; + return [area.area_id, areaName, ...area.aliases]; + }) + .flat(); + + items.push({ + id: this._formatValue({ id: floor.floor_id, type: "floor" }), type: "floor", - name: floor.name, - icon: floor.icon, - strings: [floor.floor_id, ...floor.aliases, floor.name], - level: floor.level, + primary: floorName, + floor: floor, + search_labels: [ + floor.floor_id, + floorName, + ...floor.aliases, + ...areaSearchLabels, + ], }); } - output.push( - ...floorAreas.map((area, index, array) => ({ - id: area.area_id, - type: "area" as const, - name: area.name, - icon: area.icon, - strings: [area.area_id, ...area.aliases, area.name], - hasFloor: true, - level: null, - lastArea: index === array.length - 1, - })) + items.push( + ...floorAreas.map((area) => { + const areaName = computeAreaName(area) || area.area_id; + return { + id: this._formatValue({ id: area.area_id, type: "area" }), + type: "area" as const, + primary: areaName, + area: area, + icon: area.icon || undefined, + search_labels: [area.area_id, areaName, ...area.aliases], + }; + }) ); }); - if (!output.length && !unassisgnedAreas.length) { - output.push({ - id: "no_areas", - type: "area", - name: this.hass.localize( - "ui.components.area-picker.unassigned_areas" - ), - icon: null, - strings: [], - level: null, - }); - } - - output.push( - ...unassisgnedAreas.map((area) => ({ - id: area.area_id, - type: "area" as const, - name: area.name, - icon: area.icon, - strings: [area.area_id, ...area.aliases, area.name], - level: null, - })) + items.push( + ...unassisgnedAreas.map((area) => { + const areaName = computeAreaName(area) || area.area_id; + return { + id: this._formatValue({ id: area.area_id, type: "area" }), + type: "area" as const, + primary: areaName, + icon: area.icon || undefined, + search_labels: [area.area_id, areaName, ...area.aliases], + }; + }) ); - return output; + return items; } ); - protected updated(changedProps: PropertyValues) { - if ( - (!this._init && this.hass) || - (this._init && changedProps.has("_opened") && this._opened) - ) { - this._init = true; - const areas = this._getAreas( - Object.values(this.hass.floors), - Object.values(this.hass.areas), - Object.values(this.hass.devices), - Object.values(this.hass.entities), - this.includeDomains, - this.excludeDomains, - this.includeDeviceClasses, - this.deviceFilter, - this.entityFilter, - this.excludeAreas, - this.excludeFloors - ); - this.comboBox.items = areas; - this.comboBox.filteredItems = areas; - } - } + private _rowRenderer: ComboBoxLitRenderer = ( + item, + { index }, + combobox + ) => { + const nextItem = combobox.filteredItems?.[index + 1]; + const isLastArea = + !nextItem || + nextItem.type === "floor" || + (nextItem.type === "area" && !nextItem.area?.floor_id); + + const rtl = computeRTL(this.hass); + + const hasFloor = item.type === "area" && item.area?.floor_id; + + return html` + + ${item.type === "area" && hasFloor + ? html` + + ` + : nothing} + ${item.type === "floor" && item.floor + ? html`` + : item.icon + ? html`` + : html``} + ${item.primary} + + `; + }; + + private _getItems = () => + this._getAreasAndFloors( + this.hass.floors, + this.hass.areas, + this.hass.devices, + this.hass.entities, + this.includeDomains, + this.excludeDomains, + this.includeDeviceClasses, + this.deviceFilter, + this.entityFilter, + this.excludeAreas, + this.excludeFloors + ); + + private _formatValue = memoizeOne((value: AreaFloorValue): string => + [value.type, value.id].join(SEPARATOR) + ); + + private _parseValue = memoizeOne((value: string): AreaFloorValue => { + const [type, id] = value.split(SEPARATOR); + + return { id, type: type as "floor" | "area" }; + }); protected render(): TemplateResult { + const placeholder = + this.placeholder ?? this.hass.localize("ui.components.area-picker.area"); + + const value = this.value ? this._formatValue(this.value) : undefined; + return html` - - + `; } - private _filterChanged(ev: CustomEvent): void { - const target = ev.target as HaComboBox; - const filterString = ev.detail.value; - if (!filterString) { - this.comboBox.filteredItems = this.comboBox.items; - return; - } - - const filteredItems = fuzzyFilterSort( - filterString, - target.items || [] - ); - - this.comboBox.filteredItems = filteredItems; - } - - private get _value() { - return this.value || ""; - } - - private _openedChanged(ev: ValueChangedEvent) { - this._opened = ev.detail.value; - } - - private async _areaChanged(ev: ValueChangedEvent) { + private _valueChanged(ev: ValueChangedEvent) { ev.stopPropagation(); - const newValue = ev.detail.value; + const value = ev.detail.value; - if (newValue === "no_areas") { + if (!value) { + this._setValue(undefined); return; } - const selected = this.comboBox.selectedItem; + const selected = this._parseValue(value); + this._setValue(selected); + } - fireEvent(this, "value-changed", { - value: { - id: selected.id, - type: selected.type, - }, - }); + private _setValue(value?: AreaFloorValue) { + this.value = value; + fireEvent(this, "value-changed", { value }); + fireEvent(this, "change"); } } diff --git a/src/components/ha-picker-combo-box.ts b/src/components/ha-picker-combo-box.ts index bd306806a0..5280909ef8 100644 --- a/src/components/ha-picker-combo-box.ts +++ b/src/components/ha-picker-combo-box.ts @@ -234,7 +234,7 @@ export class HaPickerComboBox extends LitElement { const searchString = ev.detail.value.trim() as string; const index = this._fuseIndex(this._items); - const fuse = new HaFuse(this._items, {}, index); + const fuse = new HaFuse(this._items, { shouldSort: false }, index); const results = fuse.multiTermsSearch(searchString); if (results) { diff --git a/src/components/ha-target-picker.ts b/src/components/ha-target-picker.ts index e7a98128b7..01624644ae 100644 --- a/src/components/ha-target-picker.ts +++ b/src/components/ha-target-picker.ts @@ -398,10 +398,12 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { .hass=${this.hass} id="input" .type=${"area_id"} - .label=${this.hass.localize( + .placeholder=${this.hass.localize( + "ui.components.target-picker.add_area_id" + )} + .searchLabel=${this.hass.localize( "ui.components.target-picker.add_area_id" )} - no-add .deviceFilter=${this.deviceFilter} .entityFilter=${this.entityFilter} .includeDeviceClasses=${this.includeDeviceClasses} From c11d2c10df4f00300386a367aa14db7ad779e1d6 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Fri, 23 May 2025 10:42:27 +0200 Subject: [PATCH 09/30] Enable keyboard sorting for items-display-editor (#25546) * Enable keyboard sorting for items-display-editor * Add alt + arrow and fix bugs * Fix alt bug * Improve selected drag highlight --- src/components/ha-items-display-editor.ts | 287 +++++++++++++++------- 1 file changed, 200 insertions(+), 87 deletions(-) diff --git a/src/components/ha-items-display-editor.ts b/src/components/ha-items-display-editor.ts index d0e1319be0..77e1dfff35 100644 --- a/src/components/ha-items-display-editor.ts +++ b/src/components/ha-items-display-editor.ts @@ -2,7 +2,7 @@ import { ResizeController } from "@lit-labs/observers/resize-controller"; import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js"; import type { TemplateResult } from "lit"; import { LitElement, css, html, nothing } from "lit"; -import { customElement, property } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; import { repeat } from "lit/directives/repeat"; @@ -64,92 +64,15 @@ export class HaItemDisplayEditor extends LitElement { item: DisplayItem ) => TemplateResult<1> | typeof nothing; + /** + * Used to sort items by keyboard navigation. + */ + @state() private _dragIndex: number | null = null; + private _showIcon = new ResizeController(this, { callback: (entries) => entries[0]?.contentRect.width > 450, }); - private _toggle(ev) { - ev.stopPropagation(); - const value = ev.currentTarget.value; - - const hiddenItems = this._hiddenItems(this.items, this.value.hidden); - - const newHidden = hiddenItems.map((item) => item.value); - - if (newHidden.includes(value)) { - newHidden.splice(newHidden.indexOf(value), 1); - } else { - newHidden.push(value); - } - - const newVisibleItems = this._visibleItems( - this.items, - newHidden, - this.value.order - ); - const newOrder = newVisibleItems.map((a) => a.value); - - this.value = { - hidden: newHidden, - order: newOrder, - }; - fireEvent(this, "value-changed", { value: this.value }); - } - - private _itemMoved(ev: CustomEvent): void { - ev.stopPropagation(); - const { oldIndex, newIndex } = ev.detail; - - const visibleItems = this._visibleItems( - this.items, - this.value.hidden, - this.value.order - ); - const newOrder = visibleItems.map((item) => item.value); - - const movedItem = newOrder.splice(oldIndex, 1)[0]; - newOrder.splice(newIndex, 0, movedItem); - - this.value = { - ...this.value, - order: newOrder, - }; - fireEvent(this, "value-changed", { value: this.value }); - } - - private _navigate(ev) { - const value = ev.currentTarget.value; - fireEvent(this, "item-display-navigate-clicked", { value }); - ev.stopPropagation(); - } - - private _visibleItems = memoizeOne( - (items: DisplayItem[], hidden: string[], order: string[]) => { - const compare = orderCompare(order); - - const visibleItems = items.filter((item) => !hidden.includes(item.value)); - if (this.dontSortVisible) { - return visibleItems; - } - - return items.sort((a, b) => - a.disableSorting && !b.disableSorting ? -1 : compare(a.value, b.value) - ); - } - ); - - private _allItems = memoizeOne( - (items: DisplayItem[], hidden: string[], order: string[]) => { - const visibleItems = this._visibleItems(items, hidden, order); - const hiddenItems = this._hiddenItems(items, hidden); - return [...visibleItems, ...hiddenItems]; - } - ); - - private _hiddenItems = memoizeOne((items: DisplayItem[], hidden: string[]) => - items.filter((item) => hidden.includes(item.value)) - ); - protected render() { const allItems = this._allItems( this.items, @@ -168,7 +91,7 @@ export class HaItemDisplayEditor extends LitElement { ${repeat( allItems, (item) => item.value, - (item: DisplayItem, _idx) => { + (item: DisplayItem, idx) => { const isVisible = !this.value.hidden.includes(item.value); const { label, @@ -180,9 +103,7 @@ export class HaItemDisplayEditor extends LitElement { } = item; return html` ${label} ${description @@ -199,6 +125,13 @@ export class HaItemDisplayEditor extends LitElement { ${isVisible && !disableSorting ? html` item.value); + + if (newHidden.includes(value)) { + newHidden.splice(newHidden.indexOf(value), 1); + } else { + newHidden.push(value); + } + + const newVisibleItems = this._visibleItems( + this.items, + newHidden, + this.value.order + ); + const newOrder = newVisibleItems.map((a) => a.value); + + this.value = { + hidden: newHidden, + order: newOrder, + }; + fireEvent(this, "value-changed", { value: this.value }); + } + + private _itemMoved(ev: CustomEvent): void { + ev.stopPropagation(); + const { oldIndex, newIndex } = ev.detail; + + this._moveItem(oldIndex, newIndex); + } + + private _moveItem(oldIndex, newIndex) { + if (oldIndex === newIndex) { + return; + } + + const visibleItems = this._visibleItems( + this.items, + this.value.hidden, + this.value.order + ); + const newOrder = visibleItems.map((item) => item.value); + + const movedItem = newOrder.splice(oldIndex, 1)[0]; + newOrder.splice(newIndex, 0, movedItem); + + this.value = { + ...this.value, + order: newOrder, + }; + fireEvent(this, "value-changed", { value: this.value }); + } + + private _navigate(ev) { + const value = ev.currentTarget.value; + fireEvent(this, "item-display-navigate-clicked", { value }); + ev.stopPropagation(); + } + + private _visibleItems = memoizeOne( + (items: DisplayItem[], hidden: string[], order: string[]) => { + const compare = orderCompare(order); + + const visibleItems = items.filter((item) => !hidden.includes(item.value)); + if (this.dontSortVisible) { + return [ + ...visibleItems.filter((item) => !item.disableSorting), + ...visibleItems.filter((item) => item.disableSorting), + ]; + } + + return items.sort((a, b) => + a.disableSorting && !b.disableSorting ? -1 : compare(a.value, b.value) + ); + } + ); + + private _allItems = memoizeOne( + (items: DisplayItem[], hidden: string[], order: string[]) => { + const visibleItems = this._visibleItems(items, hidden, order); + const hiddenItems = this._hiddenItems(items, hidden); + return [...visibleItems, ...hiddenItems]; + } + ); + + private _hiddenItems = memoizeOne((items: DisplayItem[], hidden: string[]) => + items.filter((item) => hidden.includes(item.value)) + ); + + private _maxSortableIndex = memoizeOne( + (items: DisplayItem[], hidden: string[]) => + items.filter( + (item) => !item.disableSorting && !hidden.includes(item.value) + ).length - 1 + ); + + private _keyActivatedMove = (ev: KeyboardEvent, clearDragIndex = false) => { + const oldIndex = this._dragIndex; + + if (ev.key === "ArrowUp") { + this._dragIndex = Math.max(0, this._dragIndex! - 1); + } else { + this._dragIndex = Math.min( + this._maxSortableIndex(this.items, this.value.hidden), + this._dragIndex! + 1 + ); + } + this._moveItem(oldIndex, this._dragIndex); + + // refocus the item after the sort + setTimeout(async () => { + await this.updateComplete; + const selectedElement = this.shadowRoot?.querySelector( + `ha-md-list-item:nth-child(${this._dragIndex! + 1})` + ) as HTMLElement | null; + selectedElement?.focus(); + if (clearDragIndex) { + this._dragIndex = null; + } + }); + }; + + private _sortKeydown = (ev: KeyboardEvent) => { + if ( + this._dragIndex !== null && + (ev.key === "ArrowUp" || ev.key === "ArrowDown") + ) { + ev.preventDefault(); + this._keyActivatedMove(ev); + } else if (this._dragIndex !== null && ev.key === "Escape") { + ev.preventDefault(); + ev.stopPropagation(); + this._dragIndex = null; + this.removeEventListener("keydown", this._sortKeydown); + } + }; + + private _listElementKeydown = (ev: KeyboardEvent) => { + if (ev.altKey && (ev.key === "ArrowUp" || ev.key === "ArrowDown")) { + ev.preventDefault(); + this._dragIndex = (ev.target as any).idx; + this._keyActivatedMove(ev, true); + } else if ( + (!this.showNavigationButton && ev.key === "Enter") || + ev.key === " " + ) { + this._dragHandleKeydown(ev); + } + }; + + private _dragHandleKeydown(ev: KeyboardEvent): void { + if (ev.key === "Enter" || ev.key === " ") { + ev.preventDefault(); + ev.stopPropagation(); + if (this._dragIndex === null) { + this._dragIndex = (ev.target as any).idx; + this.addEventListener("keydown", this._sortKeydown); + } else { + this.removeEventListener("keydown", this._sortKeydown); + this._dragIndex = null; + } + } + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + this.removeEventListener("keydown", this._sortKeydown); + } + static styles = css` :host { display: block; @@ -273,6 +380,12 @@ export class HaItemDisplayEditor extends LitElement { --md-list-item-two-line-container-height: 48px; --md-list-item-one-line-container-height: 48px; } + ha-md-list-item.drag-selected { + box-shadow: + 0px 0px 8px 4px rgba(var(--rgb-accent-color), 0.8), + inset 0px 2px 8px 4px rgba(var(--rgb-accent-color), 0.4); + border-radius: 8px; + } ha-md-list-item ha-icon-button { margin-left: -12px; margin-right: -12px; From 9b8be9f1afe1f1930b87374586acb8ae9770171f Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Fri, 23 May 2025 13:08:22 +0300 Subject: [PATCH 10/30] Fix chart labels for multi year periods (#25572) --- src/components/chart/ha-chart-base.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 0703d66eb9..597b5650fc 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -387,9 +387,9 @@ export class HaChartBase extends LitElement { if (axis.type !== "time" || axis.show === false) { return axis; } - if (axis.max && axis.min) { + if (axis.min) { this._minutesDifference = differenceInMinutes( - axis.max as Date, + (axis.max as Date) || new Date(), axis.min as Date ); } From 4d8176ad6e92032e1804db3fc4284c94dc210279 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 14:13:11 +0300 Subject: [PATCH 11/30] Update octokit monorepo to v8 (major) (#25573) Update octokit monorepo to v8 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 ++-- yarn.lock | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index eba1a08ffa..08ccd79d14 100644 --- a/package.json +++ b/package.json @@ -155,8 +155,8 @@ "@babel/preset-env": "7.27.2", "@bundle-stats/plugin-webpack-filter": "4.20.1", "@lokalise/node-api": "14.7.0", - "@octokit/auth-oauth-device": "7.1.5", - "@octokit/plugin-retry": "7.2.1", + "@octokit/auth-oauth-device": "8.0.0", + "@octokit/plugin-retry": "8.0.0", "@octokit/rest": "21.1.1", "@rsdoctor/rspack-plugin": "1.1.2", "@rspack/cli": "1.3.10", diff --git a/yarn.lock b/yarn.lock index e617aff51d..93b578ef53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3273,15 +3273,15 @@ __metadata: languageName: node linkType: hard -"@octokit/auth-oauth-device@npm:7.1.5": - version: 7.1.5 - resolution: "@octokit/auth-oauth-device@npm:7.1.5" +"@octokit/auth-oauth-device@npm:8.0.0": + version: 8.0.0 + resolution: "@octokit/auth-oauth-device@npm:8.0.0" dependencies: "@octokit/oauth-methods": "npm:^5.1.5" "@octokit/request": "npm:^9.2.3" "@octokit/types": "npm:^14.0.0" universal-user-agent: "npm:^7.0.0" - checksum: 10/f193f037735d997f37ab0125d82f156fd2997b2f336bb458952524d532c2f85a6b04e904cf226ab1bed88ee11223af40352f47b591fe5d40741e840e2e399a31 + checksum: 10/d37842da9d2acb40c5c3f3c4e6423db2155aa08d3cf61e1d2f56e00a76ddb15cfd08c9b046f7d2dbb493adaeb28ba886a5f20eb7c69a3a204cf93b3de6f2d8a3 languageName: node linkType: hard @@ -3392,16 +3392,16 @@ __metadata: languageName: node linkType: hard -"@octokit/plugin-retry@npm:7.2.1": - version: 7.2.1 - resolution: "@octokit/plugin-retry@npm:7.2.1" +"@octokit/plugin-retry@npm:8.0.0": + version: 8.0.0 + resolution: "@octokit/plugin-retry@npm:8.0.0" dependencies: "@octokit/request-error": "npm:^6.1.8" "@octokit/types": "npm:^14.0.0" bottleneck: "npm:^2.15.3" peerDependencies: "@octokit/core": ">=6" - checksum: 10/64346ff62c2323bb44f5274b43781bfb594d76ca82bcececf4fe4204ecb00f0ba58a92719fbbece1f75981bc00cdfbeaa29fef25ba0a67d14e8f0f46f128a3bd + checksum: 10/d1dbfaf417193a6dabaccae3ef4d667735137d56f2b7be485c4ce1812ccff1b0503e7f48ac24e53a555908fac3b1cadc5ac077296c69b4e4acf1cde8c3bb9e0b languageName: node linkType: hard @@ -9282,8 +9282,8 @@ __metadata: "@material/web": "npm:2.3.0" "@mdi/js": "npm:7.4.47" "@mdi/svg": "npm:7.4.47" - "@octokit/auth-oauth-device": "npm:7.1.5" - "@octokit/plugin-retry": "npm:7.2.1" + "@octokit/auth-oauth-device": "npm:8.0.0" + "@octokit/plugin-retry": "npm:8.0.0" "@octokit/rest": "npm:21.1.1" "@replit/codemirror-indentation-markers": "npm:6.5.3" "@rsdoctor/rspack-plugin": "npm:1.1.2" From 81ba2db93a98ebb53719ac4cd9e8a0c61684f4e1 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Fri, 23 May 2025 16:20:41 +0300 Subject: [PATCH 12/30] Custom variable down sampling for line charts (#25561) --- src/components/chart/down-sample.ts | 72 +++++++++++++++++++++++++++ src/components/chart/ha-chart-base.ts | 47 +++++++++++------ 2 files changed, 105 insertions(+), 14 deletions(-) create mode 100644 src/components/chart/down-sample.ts diff --git a/src/components/chart/down-sample.ts b/src/components/chart/down-sample.ts new file mode 100644 index 0000000000..9790de2a26 --- /dev/null +++ b/src/components/chart/down-sample.ts @@ -0,0 +1,72 @@ +import type { LineSeriesOption } from "echarts"; + +export function downSampleLineData( + data: LineSeriesOption["data"], + chartWidth: number, + minX?: number, + maxX?: number +) { + if (!data || data.length < 10) { + return data; + } + const width = chartWidth * window.devicePixelRatio; + if (data.length <= width) { + return data; + } + const min = minX ?? getPointData(data[0]!)[0]; + const max = maxX ?? getPointData(data[data.length - 1]!)[0]; + const step = Math.floor((max - min) / width); + 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 + for (const point of data) { + const pointData = getPointData(point); + if (!Array.isArray(pointData)) continue; + const x = Number(pointData[0]); + const y = Number(pointData[1]); + if (isNaN(x) || isNaN(y)) continue; + + 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 } }); + } else { + if (frame.min.y > y) { + frame.min = { point, x, y }; + } + if (frame.max.y < y) { + frame.max = { point, x, y }; + } + } + } + + // Convert frames back to points + const result: typeof data = []; + 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); + } + result.push(frame.min.point); + if (frame.min.x < frame.max.x) { + result.push(frame.max.point); + } + } + + return result; +} + +function getPointData(point: NonNullable[number]) { + const pointData = + point && typeof point === "object" && "value" in point + ? point.value + : point; + return pointData as number[]; +} diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 597b5650fc..64db12b3df 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -27,6 +27,7 @@ import "../ha-icon-button"; import { formatTimeLabel } from "./axis-label"; import { ensureArray } from "../../common/array/ensure-array"; import "../chips/ha-assist-chip"; +import { downSampleLineData } from "./down-sample"; export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; const LEGEND_OVERFLOW_LIMIT = 10; @@ -613,19 +614,21 @@ export class HaChartBase extends LitElement { } private _getSeries() { - const series = ensureArray(this.data).filter( - (d) => !this._hiddenDatasets.has(String(d.name ?? d.id)) - ); + const xAxis = (this.options?.xAxis?.[0] ?? this.options?.xAxis) as + | XAXisOption + | undefined; const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as | YAXisOption | undefined; - if (yAxis?.type === "log") { - // set <=0 values to null so they render as gaps on a log graph - return series.map((d) => - d.type === "line" - ? { - ...d, - data: d.data?.map((v) => + const series = ensureArray(this.data) + .filter((d) => !this._hiddenDatasets.has(String(d.name ?? d.id))) + .map((s) => { + if (s.type === "line") { + if (yAxis?.type === "log") { + // set <=0 values to null so they render as gaps on a log graph + return { + ...s, + data: s.data?.map((v) => Array.isArray(v) ? [ v[0], @@ -634,10 +637,26 @@ export class HaChartBase extends LitElement { ] : v ), - } - : d - ); - } + }; + } + if (s.sampling === "minmax") { + const minX = + xAxis?.min && typeof xAxis.min === "number" + ? xAxis.min + : undefined; + const maxX = + xAxis?.max && typeof xAxis.max === "number" + ? xAxis.max + : undefined; + return { + ...s, + sampling: undefined, + data: downSampleLineData(s.data, this.clientWidth, minX, maxX), + }; + } + } + return s; + }); return series; } From f563146165fe5c70a214b4a249d7188d511dc763 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 23 May 2025 11:33:47 -0400 Subject: [PATCH 13/30] Sort discoveries by title on integration page (#25578) --- .../integrations/ha-config-integration-page.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/panels/config/integrations/ha-config-integration-page.ts b/src/panels/config/integrations/ha-config-integration-page.ts index 9c9e9ba764..a60d0b32b9 100644 --- a/src/panels/config/integrations/ha-config-integration-page.ts +++ b/src/panels/config/integrations/ha-config-integration-page.ts @@ -275,9 +275,15 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) { this.configEntriesInProgress ); - const discoveryFlows = configEntriesInProgress.filter( - (flow) => !ATTENTION_SOURCES.includes(flow.context.source) - ); + const discoveryFlows = configEntriesInProgress + .filter((flow) => !ATTENTION_SOURCES.includes(flow.context.source)) + .sort((a, b) => + caseInsensitiveStringCompare( + a.localized_title || "zzz", + b.localized_title || "zzz", + this.hass.locale.language + ) + ); const attentionFlows = configEntriesInProgress.filter((flow) => ATTENTION_SOURCES.includes(flow.context.source) From 4e4a82e023103da2a0be135ba84ae435b166e29a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 17:36:56 +0200 Subject: [PATCH 14/30] Update rspack monorepo to v1.3.11 (#25574) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 +- yarn.lock | 121 +++++++++++++++++++++++++-------------------------- 2 files changed, 61 insertions(+), 64 deletions(-) diff --git a/package.json b/package.json index 08ccd79d14..79882397c0 100644 --- a/package.json +++ b/package.json @@ -159,8 +159,8 @@ "@octokit/plugin-retry": "8.0.0", "@octokit/rest": "21.1.1", "@rsdoctor/rspack-plugin": "1.1.2", - "@rspack/cli": "1.3.10", - "@rspack/core": "1.3.10", + "@rspack/cli": "1.3.11", + "@rspack/core": "1.3.11", "@types/babel__plugin-transform-runtime": "7.9.5", "@types/chromecast-caf-receiver": "6.0.21", "@types/chromecast-caf-sender": "1.0.11", diff --git a/yarn.lock b/yarn.lock index 93b578ef53..11566ab6ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3883,82 +3883,82 @@ __metadata: languageName: node linkType: hard -"@rspack/binding-darwin-arm64@npm:1.3.10": - version: 1.3.10 - resolution: "@rspack/binding-darwin-arm64@npm:1.3.10" +"@rspack/binding-darwin-arm64@npm:1.3.11": + version: 1.3.11 + resolution: "@rspack/binding-darwin-arm64@npm:1.3.11" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rspack/binding-darwin-x64@npm:1.3.10": - version: 1.3.10 - resolution: "@rspack/binding-darwin-x64@npm:1.3.10" +"@rspack/binding-darwin-x64@npm:1.3.11": + version: 1.3.11 + resolution: "@rspack/binding-darwin-x64@npm:1.3.11" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rspack/binding-linux-arm64-gnu@npm:1.3.10": - version: 1.3.10 - resolution: "@rspack/binding-linux-arm64-gnu@npm:1.3.10" +"@rspack/binding-linux-arm64-gnu@npm:1.3.11": + version: 1.3.11 + resolution: "@rspack/binding-linux-arm64-gnu@npm:1.3.11" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rspack/binding-linux-arm64-musl@npm:1.3.10": - version: 1.3.10 - resolution: "@rspack/binding-linux-arm64-musl@npm:1.3.10" +"@rspack/binding-linux-arm64-musl@npm:1.3.11": + version: 1.3.11 + resolution: "@rspack/binding-linux-arm64-musl@npm:1.3.11" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rspack/binding-linux-x64-gnu@npm:1.3.10": - version: 1.3.10 - resolution: "@rspack/binding-linux-x64-gnu@npm:1.3.10" +"@rspack/binding-linux-x64-gnu@npm:1.3.11": + version: 1.3.11 + resolution: "@rspack/binding-linux-x64-gnu@npm:1.3.11" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rspack/binding-linux-x64-musl@npm:1.3.10": - version: 1.3.10 - resolution: "@rspack/binding-linux-x64-musl@npm:1.3.10" +"@rspack/binding-linux-x64-musl@npm:1.3.11": + version: 1.3.11 + resolution: "@rspack/binding-linux-x64-musl@npm:1.3.11" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rspack/binding-win32-arm64-msvc@npm:1.3.10": - version: 1.3.10 - resolution: "@rspack/binding-win32-arm64-msvc@npm:1.3.10" +"@rspack/binding-win32-arm64-msvc@npm:1.3.11": + version: 1.3.11 + resolution: "@rspack/binding-win32-arm64-msvc@npm:1.3.11" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rspack/binding-win32-ia32-msvc@npm:1.3.10": - version: 1.3.10 - resolution: "@rspack/binding-win32-ia32-msvc@npm:1.3.10" +"@rspack/binding-win32-ia32-msvc@npm:1.3.11": + version: 1.3.11 + resolution: "@rspack/binding-win32-ia32-msvc@npm:1.3.11" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rspack/binding-win32-x64-msvc@npm:1.3.10": - version: 1.3.10 - resolution: "@rspack/binding-win32-x64-msvc@npm:1.3.10" +"@rspack/binding-win32-x64-msvc@npm:1.3.11": + version: 1.3.11 + resolution: "@rspack/binding-win32-x64-msvc@npm:1.3.11" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@rspack/binding@npm:1.3.10": - version: 1.3.10 - resolution: "@rspack/binding@npm:1.3.10" +"@rspack/binding@npm:1.3.11": + version: 1.3.11 + resolution: "@rspack/binding@npm:1.3.11" dependencies: - "@rspack/binding-darwin-arm64": "npm:1.3.10" - "@rspack/binding-darwin-x64": "npm:1.3.10" - "@rspack/binding-linux-arm64-gnu": "npm:1.3.10" - "@rspack/binding-linux-arm64-musl": "npm:1.3.10" - "@rspack/binding-linux-x64-gnu": "npm:1.3.10" - "@rspack/binding-linux-x64-musl": "npm:1.3.10" - "@rspack/binding-win32-arm64-msvc": "npm:1.3.10" - "@rspack/binding-win32-ia32-msvc": "npm:1.3.10" - "@rspack/binding-win32-x64-msvc": "npm:1.3.10" + "@rspack/binding-darwin-arm64": "npm:1.3.11" + "@rspack/binding-darwin-x64": "npm:1.3.11" + "@rspack/binding-linux-arm64-gnu": "npm:1.3.11" + "@rspack/binding-linux-arm64-musl": "npm:1.3.11" + "@rspack/binding-linux-x64-gnu": "npm:1.3.11" + "@rspack/binding-linux-x64-musl": "npm:1.3.11" + "@rspack/binding-win32-arm64-msvc": "npm:1.3.11" + "@rspack/binding-win32-ia32-msvc": "npm:1.3.11" + "@rspack/binding-win32-x64-msvc": "npm:1.3.11" dependenciesMeta: "@rspack/binding-darwin-arm64": optional: true @@ -3978,16 +3978,16 @@ __metadata: optional: true "@rspack/binding-win32-x64-msvc": optional: true - checksum: 10/10328e405c6708f7d1eea7e8f83d7f4453bcc3e4ca77b99eb29819d9f66c51e85b8cf46daab797bea59fdafc266dab6091deb9b5e2ec3007482da11ab10dc62c + checksum: 10/dbf1cc65b2d69ae1a2229fef8184ca456297a78e1a90a9c2826cc221fbbc79302eb86e2a7e66e4b3d5d2e64906d909c066511779fef6a96acadc01d89ed4a857 languageName: node linkType: hard -"@rspack/cli@npm:1.3.10": - version: 1.3.10 - resolution: "@rspack/cli@npm:1.3.10" +"@rspack/cli@npm:1.3.11": + version: 1.3.11 + resolution: "@rspack/cli@npm:1.3.11" dependencies: "@discoveryjs/json-ext": "npm:^0.5.7" - "@rspack/dev-server": "npm:1.1.1" + "@rspack/dev-server": "npm:1.1.2" colorette: "npm:2.0.20" exit-hook: "npm:^4.0.0" interpret: "npm:^3.1.1" @@ -3998,42 +3998,39 @@ __metadata: "@rspack/core": ^1.0.0-alpha || ^1.x bin: rspack: bin/rspack.js - checksum: 10/c9229775cac9fe2885522153ee993e54faf37646d8a2c121c83428aca5d63b959532f0cd46a559c67fcf23ad9e607d1506c451058173c030511206b834c5cab3 + checksum: 10/6186008b2b4cb223345d39dd69437b667bf0c35ca4fde45664536eda13d56fcf77a9fbd755436267d2c927427ebdebc8fa5b3cffd2bd2be65e885297c9c4282b languageName: node linkType: hard -"@rspack/core@npm:1.3.10": - version: 1.3.10 - resolution: "@rspack/core@npm:1.3.10" +"@rspack/core@npm:1.3.11": + version: 1.3.11 + resolution: "@rspack/core@npm:1.3.11" dependencies: "@module-federation/runtime-tools": "npm:0.13.1" - "@rspack/binding": "npm:1.3.10" + "@rspack/binding": "npm:1.3.11" "@rspack/lite-tapable": "npm:1.0.1" - caniuse-lite: "npm:^1.0.30001717" + caniuse-lite: "npm:^1.0.30001718" peerDependencies: "@swc/helpers": ">=0.5.1" peerDependenciesMeta: "@swc/helpers": optional: true - checksum: 10/5471ced4f461936c723199006182ce9fc54a5840aa7ba46f56f137d46b1839b994a9f6e4539e1376eaaf2956cbbbe5a7846d06ab61246e38711a805f7dd56df4 + checksum: 10/151964bc2ef7969076fbf1a15801009854ebf3cd09fee4385214feaa3ff8531d00bf50e5d7ebc34155a13768799549036f61902958ca6c869a34ef8d54a3f6b0 languageName: node linkType: hard -"@rspack/dev-server@npm:1.1.1": - version: 1.1.1 - resolution: "@rspack/dev-server@npm:1.1.1" +"@rspack/dev-server@npm:1.1.2": + version: 1.1.2 + resolution: "@rspack/dev-server@npm:1.1.2" dependencies: chokidar: "npm:^3.6.0" - express: "npm:^4.21.2" http-proxy-middleware: "npm:^2.0.7" - mime-types: "npm:^2.1.35" p-retry: "npm:^6.2.0" - webpack-dev-middleware: "npm:^7.4.2" webpack-dev-server: "npm:5.2.0" ws: "npm:^8.18.0" peerDependencies: "@rspack/core": "*" - checksum: 10/a712c1ed6819da26984d106e7fdede3ed720ef177bf8fa976e91eb95988b862b306c354829486dce8eabde420cb5a96cb25ecf397c9532f87f3b913e9a9da40c + checksum: 10/8dc8e806fae3f484b34eb6638c0443f8b3b9727efb8ccdd778a43c237e7dc4c04695cdffaed706a65354cb93957a60d4b42c23b7eb19155ee352aa0c914670d4 languageName: node linkType: hard @@ -6353,7 +6350,7 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001716, caniuse-lite@npm:^1.0.30001717": +"caniuse-lite@npm:^1.0.30001716, caniuse-lite@npm:^1.0.30001718": version: 1.0.30001718 resolution: "caniuse-lite@npm:1.0.30001718" checksum: 10/e172a4c156f743cc947e659f353ad9edb045725cc109a02cc792dcbf98569356ebfa4bb4356e3febf87427aab0951c34c1ee5630629334f25ae6f76de7d86fd0 @@ -9287,8 +9284,8 @@ __metadata: "@octokit/rest": "npm:21.1.1" "@replit/codemirror-indentation-markers": "npm:6.5.3" "@rsdoctor/rspack-plugin": "npm:1.1.2" - "@rspack/cli": "npm:1.3.10" - "@rspack/core": "npm:1.3.10" + "@rspack/cli": "npm:1.3.11" + "@rspack/core": "npm:1.3.11" "@shoelace-style/shoelace": "npm:2.20.1" "@swc/helpers": "npm:0.5.17" "@thomasloven/round-slider": "npm:0.6.0" @@ -11182,7 +11179,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.12, mime-types@npm:^2.1.31, mime-types@npm:^2.1.35, mime-types@npm:~2.1.17, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": +"mime-types@npm:^2.1.12, mime-types@npm:^2.1.31, mime-types@npm:~2.1.17, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: From 60ef43044bf4f2e6c3536e071f4d9062e6167fe0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 17:37:26 +0200 Subject: [PATCH 15/30] Update octokit monorepo to v8.0.1 (#25576) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 +-- yarn.lock | 89 +++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 66 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 79882397c0..28ef50db6e 100644 --- a/package.json +++ b/package.json @@ -155,8 +155,8 @@ "@babel/preset-env": "7.27.2", "@bundle-stats/plugin-webpack-filter": "4.20.1", "@lokalise/node-api": "14.7.0", - "@octokit/auth-oauth-device": "8.0.0", - "@octokit/plugin-retry": "8.0.0", + "@octokit/auth-oauth-device": "8.0.1", + "@octokit/plugin-retry": "8.0.1", "@octokit/rest": "21.1.1", "@rsdoctor/rspack-plugin": "1.1.2", "@rspack/cli": "1.3.11", diff --git a/yarn.lock b/yarn.lock index 11566ab6ba..816ff832ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3273,15 +3273,15 @@ __metadata: languageName: node linkType: hard -"@octokit/auth-oauth-device@npm:8.0.0": - version: 8.0.0 - resolution: "@octokit/auth-oauth-device@npm:8.0.0" +"@octokit/auth-oauth-device@npm:8.0.1": + version: 8.0.1 + resolution: "@octokit/auth-oauth-device@npm:8.0.1" dependencies: - "@octokit/oauth-methods": "npm:^5.1.5" - "@octokit/request": "npm:^9.2.3" + "@octokit/oauth-methods": "npm:^6.0.0" + "@octokit/request": "npm:^10.0.2" "@octokit/types": "npm:^14.0.0" universal-user-agent: "npm:^7.0.0" - checksum: 10/d37842da9d2acb40c5c3f3c4e6423db2155aa08d3cf61e1d2f56e00a76ddb15cfd08c9b046f7d2dbb493adaeb28ba886a5f20eb7c69a3a204cf93b3de6f2d8a3 + checksum: 10/b5922932f11b3ec72ebb007028748a9ad2810dfea7a3f0bd40abadbc5c72a51dd3ca2c69aa27f4762526bd5fb64fcbddd002fb35d6f768228cd161099b7c29f2 languageName: node linkType: hard @@ -3317,6 +3317,16 @@ __metadata: languageName: node linkType: hard +"@octokit/endpoint@npm:^11.0.0": + version: 11.0.0 + resolution: "@octokit/endpoint@npm:11.0.0" + dependencies: + "@octokit/types": "npm:^14.0.0" + universal-user-agent: "npm:^7.0.2" + checksum: 10/d7583a44f8560343b0fbd191aa9d2653e563cdd78f550c83cf7440a66edbe47bab6d0d6c52ae271bcbd35703356154ed590b22881aa8ee690f0d8f249ce6bde0 + languageName: node + linkType: hard + "@octokit/graphql@npm:^8.2.2": version: 8.2.2 resolution: "@octokit/graphql@npm:8.2.2" @@ -3328,22 +3338,22 @@ __metadata: languageName: node linkType: hard -"@octokit/oauth-authorization-url@npm:^7.0.0": - version: 7.1.1 - resolution: "@octokit/oauth-authorization-url@npm:7.1.1" - checksum: 10/3ef5ef3fe943f9f82c6d0686120c7481078f46594c463f492e559cdd12d74043c9295f4323d7ac9f98d7a20692694cc7263657fabbcb275d3475686133d4f4a0 +"@octokit/oauth-authorization-url@npm:^8.0.0": + version: 8.0.0 + resolution: "@octokit/oauth-authorization-url@npm:8.0.0" + checksum: 10/0623bee436aef4e20754dfd4a157b6161e2a6572f5fca6cd4687ed644737bd1aa18010aa08d77b4906f2f1cd87c7050dc7e2bacde9972e24ed3a099dc427e9e6 languageName: node linkType: hard -"@octokit/oauth-methods@npm:^5.1.5": - version: 5.1.5 - resolution: "@octokit/oauth-methods@npm:5.1.5" +"@octokit/oauth-methods@npm:^6.0.0": + version: 6.0.0 + resolution: "@octokit/oauth-methods@npm:6.0.0" dependencies: - "@octokit/oauth-authorization-url": "npm:^7.0.0" - "@octokit/request": "npm:^9.2.3" - "@octokit/request-error": "npm:^6.1.8" + "@octokit/oauth-authorization-url": "npm:^8.0.0" + "@octokit/request": "npm:^10.0.2" + "@octokit/request-error": "npm:^7.0.0" "@octokit/types": "npm:^14.0.0" - checksum: 10/50753378584c9f105687a44cbfcffe7e2019f9eaa70d8f7ff8f5bccb84602c3663bc680b8fc5cd28bc981df4e855c2678814ba8426ebb17f5b2860a93f42172f + checksum: 10/4c7d8229fe7b9f527f69d93b7c2b9551aa922a435df2bf5134513a99ac23a85287d81177fb8e19c6fadd3410598e9c66d69e0aa83ce67d35f60df3a5a3aaf36b languageName: node linkType: hard @@ -3392,16 +3402,16 @@ __metadata: languageName: node linkType: hard -"@octokit/plugin-retry@npm:8.0.0": - version: 8.0.0 - resolution: "@octokit/plugin-retry@npm:8.0.0" +"@octokit/plugin-retry@npm:8.0.1": + version: 8.0.1 + resolution: "@octokit/plugin-retry@npm:8.0.1" dependencies: - "@octokit/request-error": "npm:^6.1.8" + "@octokit/request-error": "npm:^7.0.0" "@octokit/types": "npm:^14.0.0" bottleneck: "npm:^2.15.3" peerDependencies: - "@octokit/core": ">=6" - checksum: 10/d1dbfaf417193a6dabaccae3ef4d667735137d56f2b7be485c4ce1812ccff1b0503e7f48ac24e53a555908fac3b1cadc5ac077296c69b4e4acf1cde8c3bb9e0b + "@octokit/core": ">=7" + checksum: 10/ef26a0d01f285124ca4b0cc4a5881ce8205becfcd2b5707bc2bb2d9ec07e9bf331923afef81ade234ace52d6d373e18e57b6d0839d48486abee5d0531fe974b7 languageName: node linkType: hard @@ -3414,6 +3424,28 @@ __metadata: languageName: node linkType: hard +"@octokit/request-error@npm:^7.0.0": + version: 7.0.0 + resolution: "@octokit/request-error@npm:7.0.0" + dependencies: + "@octokit/types": "npm:^14.0.0" + checksum: 10/c4370d2c31f599c1f366c480d5a02bc93442e5a0e151ec5caf0d5a5b0f0f91b50ecedc945aa6ea61b4c9ed1e89153dc7727daf4317680d33e916f829da7d141b + languageName: node + linkType: hard + +"@octokit/request@npm:^10.0.2": + version: 10.0.2 + resolution: "@octokit/request@npm:10.0.2" + dependencies: + "@octokit/endpoint": "npm:^11.0.0" + "@octokit/request-error": "npm:^7.0.0" + "@octokit/types": "npm:^14.0.0" + fast-content-type-parse: "npm:^3.0.0" + universal-user-agent: "npm:^7.0.2" + checksum: 10/eaddfd49787e8caad664a80c7c665d69bd303f90b5e6be822d571b684a4cd42bdfee29119f838fdfaed2946bc09f38219e1d7a0923388436bff0bfdd0202acca + languageName: node + linkType: hard + "@octokit/request@npm:^9.2.3": version: 9.2.3 resolution: "@octokit/request@npm:9.2.3" @@ -8320,6 +8352,13 @@ __metadata: languageName: node linkType: hard +"fast-content-type-parse@npm:^3.0.0": + version: 3.0.0 + resolution: "fast-content-type-parse@npm:3.0.0" + checksum: 10/8616a8aa6c9b4f8f4f3c90eaa4e7bfc2240cfa6f41f0eef5b5aa2b2c8b38bd9ad435f1488b6d817ffd725c54651e2777b882ae9dd59366e71e7896f1ec11d473 + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -9279,8 +9318,8 @@ __metadata: "@material/web": "npm:2.3.0" "@mdi/js": "npm:7.4.47" "@mdi/svg": "npm:7.4.47" - "@octokit/auth-oauth-device": "npm:8.0.0" - "@octokit/plugin-retry": "npm:8.0.0" + "@octokit/auth-oauth-device": "npm:8.0.1" + "@octokit/plugin-retry": "npm:8.0.1" "@octokit/rest": "npm:21.1.1" "@replit/codemirror-indentation-markers": "npm:6.5.3" "@rsdoctor/rspack-plugin": "npm:1.1.2" From 7ffb0f1e3b603df1b335dcb049b69359845f592f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 23 May 2025 18:34:53 +0200 Subject: [PATCH 16/30] rename `android-safe-area-inset` to `app-safe-area-inset` (#25575) --- src/resources/theme/main.globals.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/resources/theme/main.globals.ts b/src/resources/theme/main.globals.ts index a71b51c00a..c32cd24598 100644 --- a/src/resources/theme/main.globals.ts +++ b/src/resources/theme/main.globals.ts @@ -29,19 +29,19 @@ export const mainStyles = css` /* safe-area-insets */ --safe-area-inset-top: var( - --android-safe-area-inset-top, + --app-safe-area-inset-top, env(safe-area-inset-top, 0) ); --safe-area-inset-bottom: var( - --android-safe-area-inset-bottom, + --app-safe-area-inset-bottom, env(safe-area-inset-bottom, 0) ); --safe-area-inset-left: var( - --android-safe-area-inset-left, + --app-safe-area-inset-left, env(safe-area-inset-left, 0) ); --safe-area-inset-right: var( - --android-safe-area-inset-right, + --app-safe-area-inset-right, env(safe-area-inset-right, 0) ); } From 113cc118cfd1cca6944f5b70e967195e30c67844 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Fri, 23 May 2025 10:52:42 -0700 Subject: [PATCH 17/30] Support for disabled selectors in config flow (#22592) * Support for disabled selectors in config flow * rename flag to readonly * rename to read_only * Merge branch 'dev' of https://github.com/home-assistant/frontend into disabled-fields-config-flow * rework for new backend * Fix disabled entity picker * no longer used type * Update src/dialogs/config-flow/step-flow-form.ts Co-authored-by: Petar Petrov --------- Co-authored-by: Petar Petrov --- src/components/entity/ha-entity-picker.ts | 1 + src/dialogs/config-flow/step-flow-form.ts | 25 +++++++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index 5968b4a103..ef39a4ef14 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -386,6 +386,7 @@ export class HaEntityPicker extends LitElement { return html` + schema?.map((field) => ({ + ...field, + ...(Object.values((field as HaFormSelector)?.selector ?? {})[0]?.read_only + ? { disabled: true } + : {}), + })) + ); + protected render(): TemplateResult { const step = this.step; const stepData = this._stepDataProcessed; @@ -53,7 +66,9 @@ class StepFlowForm extends LitElement { .data=${stepData} .disabled=${this._loading} @value-changed=${this._stepDataChanged} - .schema=${autocompleteLoginFields(step.data_schema)} + .schema=${autocompleteLoginFields( + this.handleReadOnlyFields(step.data_schema) + )} .error=${step.errors} .computeLabel=${this._labelCallback} .computeHelper=${this._helperCallback} @@ -178,8 +193,10 @@ class StepFlowForm extends LitElement { Object.keys(stepData).forEach((key) => { const value = stepData[key]; const isEmpty = [undefined, ""].includes(value); - - if (!isEmpty) { + const field = this.step.data_schema?.find((f) => f.name === key); + const selector = (field as HaFormSelector)?.selector ?? {}; + const read_only = (Object.values(selector)[0] as any)?.read_only; + if (!isEmpty && !read_only) { toSendData[key] = value; } }); From 549451eccb1808c8f25f05cc080d0b8facf8244f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 08:26:06 +0200 Subject: [PATCH 18/30] Bump relative-ci/agent-action from 2.2.0 to 3.0.0 (#25593) Bumps [relative-ci/agent-action](https://github.com/relative-ci/agent-action) from 2.2.0 to 3.0.0. - [Release notes](https://github.com/relative-ci/agent-action/releases) - [Commits](https://github.com/relative-ci/agent-action/compare/v2.2.0...v3.0.0) --- updated-dependencies: - dependency-name: relative-ci/agent-action dependency-version: 3.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/relative-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/relative-ci.yaml b/.github/workflows/relative-ci.yaml index 627215abb9..e82fe096c5 100644 --- a/.github/workflows/relative-ci.yaml +++ b/.github/workflows/relative-ci.yaml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Send bundle stats and build information to RelativeCI - uses: relative-ci/agent-action@v2.2.0 + uses: relative-ci/agent-action@v3.0.0 with: key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }} token: ${{ github.token }} From 28fe60f02b6f1bb0192df2b33d0a9cea8afad02d Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 26 May 2025 10:45:22 +0200 Subject: [PATCH 19/30] Improve broken cards on dashboards (#25557) --- src/panels/calendar/ha-full-calendar.ts | 17 +-- .../hui-energy-carbon-consumed-gauge-card.ts | 2 +- .../lovelace/cards/hui-alarm-panel-card.ts | 2 +- src/panels/lovelace/cards/hui-area-card.ts | 2 +- src/panels/lovelace/cards/hui-button-card.ts | 2 +- .../lovelace/cards/hui-calendar-card.ts | 11 +- src/panels/lovelace/cards/hui-card.ts | 13 ++- src/panels/lovelace/cards/hui-entity-card.ts | 2 +- src/panels/lovelace/cards/hui-error-card.ts | 104 +++++++++++++----- src/panels/lovelace/cards/hui-gauge-card.ts | 2 +- .../lovelace/cards/hui-humidifier-card.ts | 2 +- src/panels/lovelace/cards/hui-light-card.ts | 2 +- src/panels/lovelace/cards/hui-logbook-card.ts | 2 +- .../lovelace/cards/hui-media-control-card.ts | 2 +- src/panels/lovelace/cards/hui-picture-card.ts | 2 +- .../lovelace/cards/hui-picture-entity-card.ts | 2 +- .../lovelace/cards/hui-plant-status-card.ts | 2 +- .../lovelace/cards/hui-thermostat-card.ts | 2 +- src/panels/lovelace/cards/hui-tile-card.ts | 19 +--- .../lovelace/cards/hui-todo-list-card.ts | 2 +- .../cards/hui-weather-forecast-card.ts | 2 +- src/panels/lovelace/cards/types.ts | 5 +- .../components/hui-generic-entity-row.ts | 2 +- src/panels/lovelace/components/hui-warning.ts | 16 ++- .../create-element/create-element-base.ts | 20 ++-- .../entity-rows/hui-button-entity-row.ts | 2 +- .../entity-rows/hui-climate-entity-row.ts | 2 +- .../entity-rows/hui-cover-entity-row.ts | 2 +- .../entity-rows/hui-date-entity-row.ts | 2 +- .../entity-rows/hui-datetime-entity-row.ts | 2 +- .../entity-rows/hui-event-entity-row.ts | 2 +- .../entity-rows/hui-group-entity-row.ts | 2 +- .../entity-rows/hui-humidifier-entity-row.ts | 2 +- .../hui-input-button-entity-row.ts | 2 +- .../hui-input-datetime-entity-row.ts | 2 +- .../hui-input-number-entity-row.ts | 2 +- .../hui-input-select-entity-row.ts | 2 +- .../entity-rows/hui-input-text-entity-row.ts | 2 +- .../entity-rows/hui-lock-entity-row.ts | 2 +- .../hui-media-player-entity-row.ts | 2 +- .../entity-rows/hui-number-entity-row.ts | 2 +- .../entity-rows/hui-scene-entity-row.ts | 2 +- .../entity-rows/hui-script-entity-row.ts | 2 +- .../entity-rows/hui-select-entity-row.ts | 2 +- .../entity-rows/hui-sensor-entity-row.ts | 2 +- .../entity-rows/hui-simple-entity-row.ts | 2 +- .../entity-rows/hui-text-entity-row.ts | 2 +- .../entity-rows/hui-time-entity-row.ts | 2 +- .../entity-rows/hui-timer-entity-row.ts | 2 +- .../entity-rows/hui-toggle-entity-row.ts | 2 +- .../entity-rows/hui-update-entity-row.ts | 2 +- .../entity-rows/hui-valve-entity-row.ts | 2 +- .../entity-rows/hui-weather-entity-row.ts | 2 +- .../special-rows/hui-attribute-row.ts | 2 +- src/panels/lovelace/views/hui-panel-view.ts | 2 +- src/translations/en.json | 10 +- 56 files changed, 170 insertions(+), 139 deletions(-) diff --git a/src/panels/calendar/ha-full-calendar.ts b/src/panels/calendar/ha-full-calendar.ts index 4d9973f86b..410aa4bdfd 100644 --- a/src/panels/calendar/ha-full-calendar.ts +++ b/src/panels/calendar/ha-full-calendar.ts @@ -41,6 +41,7 @@ import type { } from "../../types"; import { showCalendarEventDetailDialog } from "./show-dialog-calendar-event-detail"; import { showCalendarEventEditDialog } from "./show-dialog-calendar-event-editor"; +import "../lovelace/components/hui-warning"; declare global { interface HTMLElementTagNameMap { @@ -126,11 +127,8 @@ export class HAFullCalendar extends LitElement { ${this.calendar ? html` ${this.error - ? html`${this.error}${this.error}` : ""}
@@ -420,10 +418,6 @@ export class HAFullCalendar extends LitElement { ); }); - private _clearError() { - this.error = undefined; - } - static get styles(): CSSResultGroup { return [ haStyle, @@ -510,11 +504,6 @@ export class HAFullCalendar extends LitElement { z-index: 1; } - ha-alert { - display: block; - margin: 4px 0; - } - #calendar { flex-grow: 1; background-color: var( diff --git a/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts index f7a22b9ae2..a3aa11576d 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts @@ -86,7 +86,7 @@ class HuiEnergyCarbonGaugeCard const co2State = this.hass.states[this._data.co2SignalEntity]; if (!co2State) { - return html` + return html` ${createEntityNotFoundWarning(this.hass, this._data.co2SignalEntity)} `; } diff --git a/src/panels/lovelace/cards/hui-alarm-panel-card.ts b/src/panels/lovelace/cards/hui-alarm-panel-card.ts index 01b980aebc..0a6cd85976 100644 --- a/src/panels/lovelace/cards/hui-alarm-panel-card.ts +++ b/src/panels/lovelace/cards/hui-alarm-panel-card.ts @@ -220,7 +220,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/cards/hui-area-card.ts b/src/panels/lovelace/cards/hui-area-card.ts index fc06ebd0ac..44c60b6fce 100644 --- a/src/panels/lovelace/cards/hui-area-card.ts +++ b/src/panels/lovelace/cards/hui-area-card.ts @@ -363,7 +363,7 @@ export class HuiAreaCard if (area === null) { return html` - + ${this.hass.localize("ui.card.area.area_not_found")} `; diff --git a/src/panels/lovelace/cards/hui-button-card.ts b/src/panels/lovelace/cards/hui-button-card.ts index 9b6dfcc812..f6cc9b0bcd 100644 --- a/src/panels/lovelace/cards/hui-button-card.ts +++ b/src/panels/lovelace/cards/hui-button-card.ts @@ -179,7 +179,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard { if (this._config.entity && !stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/cards/hui-calendar-card.ts b/src/panels/lovelace/cards/hui-calendar-card.ts index b549c05596..3ccadf743d 100644 --- a/src/panels/lovelace/cards/hui-calendar-card.ts +++ b/src/panels/lovelace/cards/hui-calendar-card.ts @@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators"; import { getColorByIndex } from "../../../common/color/colors"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import type { HASSDomEvent } from "../../../common/dom/fire_event"; -import { computeStateName } from "../../../common/entity/compute_state_name"; import { debounce } from "../../../common/util/debounce"; import "../../../components/ha-card"; import type { Calendar, CalendarEvent } from "../../../data/calendar"; @@ -176,17 +175,9 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard { this._events = result.events; if (result.errors.length > 0) { - const nameList = result.errors - .map((error_entity_id) => - this.hass!.states[error_entity_id] - ? computeStateName(this.hass!.states[error_entity_id]) - : error_entity_id - ) - .join(", "); - this._error = `${this.hass!.localize( "ui.components.calendar.event_retrieval_error" - )} ${nameList}`; + )}`; } } diff --git a/src/panels/lovelace/cards/hui-card.ts b/src/panels/lovelace/cards/hui-card.ts index 9f6ec0b9c7..dd3bcbeb30 100644 --- a/src/panels/lovelace/cards/hui-card.ts +++ b/src/panels/lovelace/cards/hui-card.ts @@ -13,7 +13,6 @@ import { checkConditionsMet, } from "../common/validate-condition"; import { createCardElement } from "../create-element/create-card-element"; -import { createErrorCardConfig } from "../create-element/create-element-base"; import type { LovelaceCard, LovelaceGridOptions } from "../types"; declare global { @@ -191,7 +190,9 @@ export class HuiCard extends ReactiveElement { this._element.hass = this.hass; } } catch (e: any) { - this._loadElement(createErrorCardConfig(e.message, null)); + // eslint-disable-next-line no-console + console.error(this.config?.type, e); + this._loadElement({ type: "error" }); } } if (changedProps.has("preview")) { @@ -200,7 +201,9 @@ export class HuiCard extends ReactiveElement { // For backwards compatibility (this._element as any).editMode = this.preview; } catch (e: any) { - this._loadElement(createErrorCardConfig(e.message, null)); + // eslint-disable-next-line no-console + console.error(this.config?.type, e); + this._loadElement({ type: "error" }); } } if (changedProps.has("layout")) { @@ -209,7 +212,9 @@ export class HuiCard extends ReactiveElement { // For backwards compatibility (this._element as any).isPanel = this.layout === "panel"; } catch (e: any) { - this._loadElement(createErrorCardConfig(e.message, null)); + // eslint-disable-next-line no-console + console.error(this.config?.type, e); + this._loadElement({ type: "error" }); } } } diff --git a/src/panels/lovelace/cards/hui-entity-card.ts b/src/panels/lovelace/cards/hui-entity-card.ts index 826520a080..ae51480f36 100644 --- a/src/panels/lovelace/cards/hui-entity-card.ts +++ b/src/panels/lovelace/cards/hui-entity-card.ts @@ -114,7 +114,7 @@ export class HuiEntityCard extends LitElement implements LovelaceCard { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/cards/hui-error-card.ts b/src/panels/lovelace/cards/hui-error-card.ts index d2b0ae05b8..837f78e367 100644 --- a/src/panels/lovelace/cards/hui-error-card.ts +++ b/src/panels/lovelace/cards/hui-error-card.ts @@ -1,52 +1,106 @@ -import { dump } from "js-yaml"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; -import "../../../components/ha-alert"; +import { mdiAlertCircleOutline, mdiAlertOutline } from "@mdi/js"; import type { HomeAssistant } from "../../../types"; -import type { LovelaceCard } from "../types"; +import type { LovelaceCard, LovelaceGridOptions } from "../types"; import type { ErrorCardConfig } from "./types"; +import "../../../components/ha-card"; +import "../../../components/ha-svg-icon"; + +const ERROR_ICONS = { + warning: mdiAlertOutline, + error: mdiAlertCircleOutline, +}; @customElement("hui-error-card") export class HuiErrorCard extends LitElement implements LovelaceCard { - public hass?: HomeAssistant; + @property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public preview = false; + @property({ attribute: "severity" }) public severity: "warning" | "error" = + "error"; + @state() private _config?: ErrorCardConfig; public getCardSize(): number { - return 4; + return 1; + } + + public getGridOptions(): LovelaceGridOptions { + return { + columns: 6, + rows: 1, + min_rows: 1, + min_columns: 6, + }; } public setConfig(config: ErrorCardConfig): void { this._config = config; + this.severity = config.severity || "error"; } protected render() { - if (!this._config) { - return nothing; - } + const error = + this._config?.error || + this.hass?.localize("ui.errors.config.configuration_error"); + const showTitle = this.hass === undefined || this.hass?.user?.is_admin; - let dumped: string | undefined; - - if (this._config.origConfig) { - try { - dumped = dump(this._config.origConfig); - } catch (_err: any) { - dumped = `[Error dumping ${this._config.origConfig}]`; - } - } - - return html` - ${dumped ? html`
${dumped}
` : ""} -
`; + return html` + +
+ + + +
+ ${showTitle + ? html`
${error}
` + : nothing} +
+ `; } static styles = css` - pre { - font-family: var(--ha-font-family-code); - white-space: break-spaces; - user-select: text; + ha-card { + height: 100%; + border-width: 0; + display: flex; + align-items: center; + column-gap: 16px; + padding: 16px; + } + ha-card::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + opacity: 0.12; + pointer-events: none; + content: ""; + border-radius: var(--ha-card-border-radius, 12px); + } + .no-title { + justify-content: center; + } + .title { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-weight: var(--ha-font-weight-bold); + } + ha-card.warning > .icon { + color: var(--warning-color); + } + ha-card.warning::after { + background-color: var(--warning-color); + } + ha-card.error > .icon { + color: var(--error-color); + } + ha-card.error::after { + background-color: var(--error-color); } `; } diff --git a/src/panels/lovelace/cards/hui-gauge-card.ts b/src/panels/lovelace/cards/hui-gauge-card.ts index acaf2519b2..bd21942d1d 100644 --- a/src/panels/lovelace/cards/hui-gauge-card.ts +++ b/src/panels/lovelace/cards/hui-gauge-card.ts @@ -90,7 +90,7 @@ class HuiGaugeCard extends LitElement implements LovelaceCard { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/cards/hui-humidifier-card.ts b/src/panels/lovelace/cards/hui-humidifier-card.ts index 39df5b5e8d..5ed6ad3e3a 100644 --- a/src/panels/lovelace/cards/hui-humidifier-card.ts +++ b/src/panels/lovelace/cards/hui-humidifier-card.ts @@ -121,7 +121,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/cards/hui-light-card.ts b/src/panels/lovelace/cards/hui-light-card.ts index 6e9bae3b06..263d92faaf 100644 --- a/src/panels/lovelace/cards/hui-light-card.ts +++ b/src/panels/lovelace/cards/hui-light-card.ts @@ -82,7 +82,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/cards/hui-logbook-card.ts b/src/panels/lovelace/cards/hui-logbook-card.ts index 12ee971d1f..65d5adb6a2 100644 --- a/src/panels/lovelace/cards/hui-logbook-card.ts +++ b/src/panels/lovelace/cards/hui-logbook-card.ts @@ -174,7 +174,7 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard { if (!isComponentLoaded(this.hass, "logbook")) { return html` - + ${this.hass.localize("ui.components.logbook.not_loaded", { platform: "logbook", })} + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/cards/hui-picture-card.ts b/src/panels/lovelace/cards/hui-picture-card.ts index 8bc3afb40a..ffcb51f84e 100644 --- a/src/panels/lovelace/cards/hui-picture-card.ts +++ b/src/panels/lovelace/cards/hui-picture-card.ts @@ -100,7 +100,7 @@ export class HuiPictureCard extends LitElement implements LovelaceCard { if (this._config.image_entity) { stateObj = this.hass.states[this._config.image_entity]; if (!stateObj) { - return html` + return html` ${createEntityNotFoundWarning(this.hass, this._config.image_entity)} `; } diff --git a/src/panels/lovelace/cards/hui-picture-entity-card.ts b/src/panels/lovelace/cards/hui-picture-entity-card.ts index fb7abab346..6960b4def4 100644 --- a/src/panels/lovelace/cards/hui-picture-entity-card.ts +++ b/src/panels/lovelace/cards/hui-picture-entity-card.ts @@ -119,7 +119,7 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/cards/hui-plant-status-card.ts b/src/panels/lovelace/cards/hui-plant-status-card.ts index e9f5fa7abf..5c52a6c839 100644 --- a/src/panels/lovelace/cards/hui-plant-status-card.ts +++ b/src/panels/lovelace/cards/hui-plant-status-card.ts @@ -104,7 +104,7 @@ class HuiPlantStatusCard extends LitElement implements LovelaceCard { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/cards/hui-thermostat-card.ts b/src/panels/lovelace/cards/hui-thermostat-card.ts index 8bd478e90e..3e6853e405 100644 --- a/src/panels/lovelace/cards/hui-thermostat-card.ts +++ b/src/panels/lovelace/cards/hui-thermostat-card.ts @@ -113,7 +113,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/cards/hui-tile-card.ts b/src/panels/lovelace/cards/hui-tile-card.ts index ee294c8227..d0c48dc8b4 100644 --- a/src/panels/lovelace/cards/hui-tile-card.ts +++ b/src/panels/lovelace/cards/hui-tile-card.ts @@ -1,4 +1,3 @@ -import { mdiExclamationThick, mdiHelp } from "@mdi/js"; import type { HassEntity } from "home-assistant-js-websocket"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -37,6 +36,7 @@ import type { } from "../types"; import { renderTileBadge } from "./tile/badges/tile-badge"; import type { TileCardConfig } from "./types"; +import { createEntityNotFoundWarning } from "../components/hui-warning"; export const getEntityDefaultTileIconAction = (entityId: string) => { const domain = computeDomain(entityId); @@ -249,20 +249,9 @@ export class HuiTileCard extends LitElement implements LovelaceCard { if (!stateObj) { return html` - -
- - - - - - - -
-
+ + ${createEntityNotFoundWarning(this.hass, this._config.entity)} + `; } diff --git a/src/panels/lovelace/cards/hui-todo-list-card.ts b/src/panels/lovelace/cards/hui-todo-list-card.ts index c483c7c5e1..0815821847 100644 --- a/src/panels/lovelace/cards/hui-todo-list-card.ts +++ b/src/panels/lovelace/cards/hui-todo-list-card.ts @@ -241,7 +241,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._entityId)} `; diff --git a/src/panels/lovelace/cards/hui-weather-forecast-card.ts b/src/panels/lovelace/cards/hui-weather-forecast-card.ts index 4400c0539c..4b9d49c809 100644 --- a/src/panels/lovelace/cards/hui-weather-forecast-card.ts +++ b/src/panels/lovelace/cards/hui-weather-forecast-card.ts @@ -210,7 +210,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 8e7dffe417..39e7ca7d55 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -215,8 +215,9 @@ export interface EntityFilterCardConfig extends LovelaceCardConfig { } export interface ErrorCardConfig extends LovelaceCardConfig { - error: string; - origConfig: LovelaceCardConfig; + error?: string; + origConfig?: LovelaceCardConfig; + severity?: "warning" | "error"; } export interface SeverityConfig { diff --git a/src/panels/lovelace/components/hui-generic-entity-row.ts b/src/panels/lovelace/components/hui-generic-entity-row.ts index 26c51b773b..1e762dea67 100644 --- a/src/panels/lovelace/components/hui-generic-entity-row.ts +++ b/src/panels/lovelace/components/hui-generic-entity-row.ts @@ -46,7 +46,7 @@ export class HuiGenericEntityRow extends LitElement { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this.config.entity)} `; diff --git a/src/panels/lovelace/components/hui-warning.ts b/src/panels/lovelace/components/hui-warning.ts index 3d464a0e6d..2f4cd498ad 100644 --- a/src/panels/lovelace/components/hui-warning.ts +++ b/src/panels/lovelace/components/hui-warning.ts @@ -1,24 +1,28 @@ import { STATE_NOT_RUNNING } from "home-assistant-js-websocket"; import type { TemplateResult } from "lit"; import { html, LitElement } from "lit"; -import { customElement } from "lit/decorators"; +import { customElement, property } from "lit/decorators"; import "../../../components/ha-alert"; import type { HomeAssistant } from "../../../types"; +import "../cards/hui-error-card"; export const createEntityNotFoundWarning = ( hass: HomeAssistant, - entityId: string + // left for backwards compatibility for custom cards + _entityId: string ) => hass.config.state !== STATE_NOT_RUNNING - ? hass.localize("ui.panel.lovelace.warning.entity_not_found", { - entity: entityId || "[empty]", - }) + ? hass.localize("ui.card.common.entity_not_found") : hass.localize("ui.panel.lovelace.warning.starting"); @customElement("hui-warning") export class HuiWarning extends LitElement { + @property({ attribute: false }) public hass?: HomeAssistant; + protected render(): TemplateResult { - return html` `; + return html``; } } diff --git a/src/panels/lovelace/create-element/create-element-base.ts b/src/panels/lovelace/create-element/create-element-base.ts index d509c18a85..07c73fffe6 100644 --- a/src/panels/lovelace/create-element/create-element-base.ts +++ b/src/panels/lovelace/create-element/create-element-base.ts @@ -16,7 +16,10 @@ import type { ErrorCardConfig } from "../cards/types"; import type { LovelaceElement, LovelaceElementConfig } from "../elements/types"; import type { LovelaceRow, LovelaceRowConfig } from "../entity-rows/types"; import type { LovelaceHeaderFooterConfig } from "../header-footer/types"; -import type { LovelaceHeadingBadgeConfig } from "../heading-badges/types"; +import type { + ErrorBadgeConfig as ErrorHeadingBadgeConfig, + LovelaceHeadingBadgeConfig, +} from "../heading-badges/types"; import type { LovelaceBadge, LovelaceBadgeConstructor, @@ -31,6 +34,7 @@ import type { LovelaceHeadingBadgeConstructor, LovelaceRowConstructor, } from "../types"; +import type { ErrorBadgeConfig } from "../badges/types"; const TIMEOUT = 2000; @@ -96,7 +100,7 @@ export const createErrorCardElement = (config: ErrorCardConfig) => { return el; }; -export const createErrorBadgeElement = (config: ErrorCardConfig) => { +export const createErrorBadgeElement = (config: ErrorBadgeConfig) => { const el = document.createElement("hui-error-badge"); if (customElements.get("hui-error-badge")) { el.setConfig(config); @@ -110,7 +114,9 @@ export const createErrorBadgeElement = (config: ErrorCardConfig) => { return el; }; -export const createErrorHeadingBadgeElement = (config: ErrorCardConfig) => { +export const createErrorHeadingBadgeElement = ( + config: ErrorHeadingBadgeConfig +) => { const el = document.createElement("hui-error-heading-badge"); if (customElements.get("hui-error-heading-badge")) { el.setConfig(config); @@ -124,12 +130,6 @@ export const createErrorHeadingBadgeElement = (config: ErrorCardConfig) => { return el; }; -export const createErrorCardConfig = (error, origConfig) => ({ - type: "error", - error, - origConfig, -}); - export const createErrorBadgeConfig = (error, origConfig) => ({ type: "error", error, @@ -167,7 +167,7 @@ const _createErrorElement = ( createErrorHeadingBadgeConfig(error, config) ); } - return createErrorCardElement(createErrorCardConfig(error, config)); + return createErrorCardElement({ type: "error" }); }; const _customCreate = ( diff --git a/src/panels/lovelace/entity-rows/hui-button-entity-row.ts b/src/panels/lovelace/entity-rows/hui-button-entity-row.ts index e41c4de02d..d79edc4abd 100644 --- a/src/panels/lovelace/entity-rows/hui-button-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-button-entity-row.ts @@ -36,7 +36,7 @@ class HuiButtonEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-climate-entity-row.ts b/src/panels/lovelace/entity-rows/hui-climate-entity-row.ts index 82b1a3a25f..dd655b2633 100644 --- a/src/panels/lovelace/entity-rows/hui-climate-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-climate-entity-row.ts @@ -35,7 +35,7 @@ class HuiClimateEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-cover-entity-row.ts b/src/panels/lovelace/entity-rows/hui-cover-entity-row.ts index 7658be502e..ba6a598e4c 100644 --- a/src/panels/lovelace/entity-rows/hui-cover-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-cover-entity-row.ts @@ -37,7 +37,7 @@ class HuiCoverEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-date-entity-row.ts b/src/panels/lovelace/entity-rows/hui-date-entity-row.ts index 473d06bbb9..6f964fb320 100644 --- a/src/panels/lovelace/entity-rows/hui-date-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-date-entity-row.ts @@ -36,7 +36,7 @@ class HuiDateEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; 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 f2de95979a..20581cd63e 100644 --- a/src/panels/lovelace/entity-rows/hui-datetime-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-datetime-entity-row.ts @@ -39,7 +39,7 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-event-entity-row.ts b/src/panels/lovelace/entity-rows/hui-event-entity-row.ts index 8e73f757cf..370ebda972 100644 --- a/src/panels/lovelace/entity-rows/hui-event-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-event-entity-row.ts @@ -45,7 +45,7 @@ class HuiEventEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-group-entity-row.ts b/src/panels/lovelace/entity-rows/hui-group-entity-row.ts index a51bcc8d93..091d0aa62e 100644 --- a/src/panels/lovelace/entity-rows/hui-group-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-group-entity-row.ts @@ -49,7 +49,7 @@ class HuiGroupEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-humidifier-entity-row.ts b/src/panels/lovelace/entity-rows/hui-humidifier-entity-row.ts index b8c14a8d64..551ef6bd1b 100644 --- a/src/panels/lovelace/entity-rows/hui-humidifier-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-humidifier-entity-row.ts @@ -37,7 +37,7 @@ class HuiHumidifierEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-input-button-entity-row.ts b/src/panels/lovelace/entity-rows/hui-input-button-entity-row.ts index d869d98abd..f7bb6b01ff 100644 --- a/src/panels/lovelace/entity-rows/hui-input-button-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-input-button-entity-row.ts @@ -36,7 +36,7 @@ class HuiInputButtonEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts b/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts index 623f07a7b0..b234f79cab 100644 --- a/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts @@ -41,7 +41,7 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-input-number-entity-row.ts b/src/panels/lovelace/entity-rows/hui-input-number-entity-row.ts index 2508d5efd9..ca093dd208 100644 --- a/src/panels/lovelace/entity-rows/hui-input-number-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-input-number-entity-row.ts @@ -65,7 +65,7 @@ class HuiInputNumberEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts b/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts index 99fa0a443d..30d0f1c0ae 100644 --- a/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts @@ -45,7 +45,7 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-input-text-entity-row.ts b/src/panels/lovelace/entity-rows/hui-input-text-entity-row.ts index 73f00a7a11..a490abac23 100644 --- a/src/panels/lovelace/entity-rows/hui-input-text-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-input-text-entity-row.ts @@ -37,7 +37,7 @@ class HuiInputTextEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-lock-entity-row.ts b/src/panels/lovelace/entity-rows/hui-lock-entity-row.ts index 6832139051..ab15001ea4 100644 --- a/src/panels/lovelace/entity-rows/hui-lock-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-lock-entity-row.ts @@ -37,7 +37,7 @@ class HuiLockEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-media-player-entity-row.ts b/src/panels/lovelace/entity-rows/hui-media-player-entity-row.ts index b08504c470..8a2b2c3835 100644 --- a/src/panels/lovelace/entity-rows/hui-media-player-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-media-player-entity-row.ts @@ -92,7 +92,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-number-entity-row.ts b/src/panels/lovelace/entity-rows/hui-number-entity-row.ts index 311f4493f7..3b95ef8c9e 100644 --- a/src/panels/lovelace/entity-rows/hui-number-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-number-entity-row.ts @@ -65,7 +65,7 @@ class HuiNumberEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-scene-entity-row.ts b/src/panels/lovelace/entity-rows/hui-scene-entity-row.ts index 55b8e62b86..15c25fd7df 100644 --- a/src/panels/lovelace/entity-rows/hui-scene-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-scene-entity-row.ts @@ -38,7 +38,7 @@ class HuiSceneEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-script-entity-row.ts b/src/panels/lovelace/entity-rows/hui-script-entity-row.ts index bbf1fba097..1a965fa44a 100644 --- a/src/panels/lovelace/entity-rows/hui-script-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-script-entity-row.ts @@ -39,7 +39,7 @@ class HuiScriptEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-select-entity-row.ts b/src/panels/lovelace/entity-rows/hui-select-entity-row.ts index fa7fb03ca2..53a6eb3a55 100644 --- a/src/panels/lovelace/entity-rows/hui-select-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-select-entity-row.ts @@ -45,7 +45,7 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-sensor-entity-row.ts b/src/panels/lovelace/entity-rows/hui-sensor-entity-row.ts index df47cd3690..8fb50ac831 100644 --- a/src/panels/lovelace/entity-rows/hui-sensor-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-sensor-entity-row.ts @@ -42,7 +42,7 @@ class HuiSensorEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-simple-entity-row.ts b/src/panels/lovelace/entity-rows/hui-simple-entity-row.ts index 00c8f2910e..2741a2519c 100644 --- a/src/panels/lovelace/entity-rows/hui-simple-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-simple-entity-row.ts @@ -34,7 +34,7 @@ class HuiSimpleEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-text-entity-row.ts b/src/panels/lovelace/entity-rows/hui-text-entity-row.ts index d593225ad3..e801543857 100644 --- a/src/panels/lovelace/entity-rows/hui-text-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-text-entity-row.ts @@ -40,7 +40,7 @@ class HuiTextEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-time-entity-row.ts b/src/panels/lovelace/entity-rows/hui-time-entity-row.ts index 6650b0a21e..b5050badf0 100644 --- a/src/panels/lovelace/entity-rows/hui-time-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-time-entity-row.ts @@ -37,7 +37,7 @@ class HuiTimeEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-timer-entity-row.ts b/src/panels/lovelace/entity-rows/hui-timer-entity-row.ts index 514ce69a9a..1f48a00fa4 100644 --- a/src/panels/lovelace/entity-rows/hui-timer-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-timer-entity-row.ts @@ -30,7 +30,7 @@ class HuiTimerEntityRow extends LitElement { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-toggle-entity-row.ts b/src/panels/lovelace/entity-rows/hui-toggle-entity-row.ts index 1df87198d4..63ccda2389 100644 --- a/src/panels/lovelace/entity-rows/hui-toggle-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-toggle-entity-row.ts @@ -35,7 +35,7 @@ class HuiToggleEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-update-entity-row.ts b/src/panels/lovelace/entity-rows/hui-update-entity-row.ts index 2ad19e2274..17eb6818d3 100644 --- a/src/panels/lovelace/entity-rows/hui-update-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-update-entity-row.ts @@ -38,7 +38,7 @@ class HuiUpdateEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-valve-entity-row.ts b/src/panels/lovelace/entity-rows/hui-valve-entity-row.ts index 69c0e42883..d291fa2090 100644 --- a/src/panels/lovelace/entity-rows/hui-valve-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-valve-entity-row.ts @@ -35,7 +35,7 @@ class HuiValveEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts b/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts index 856d9d6990..e8cdacd5d2 100644 --- a/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts @@ -105,7 +105,7 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/special-rows/hui-attribute-row.ts b/src/panels/lovelace/special-rows/hui-attribute-row.ts index 6ad3e3d75a..77b4411271 100644 --- a/src/panels/lovelace/special-rows/hui-attribute-row.ts +++ b/src/panels/lovelace/special-rows/hui-attribute-row.ts @@ -42,7 +42,7 @@ class HuiAttributeRow extends LitElement implements LovelaceRow { if (!stateObj) { return html` - + ${createEntityNotFoundWarning(this.hass, this._config.entity)} `; diff --git a/src/panels/lovelace/views/hui-panel-view.ts b/src/panels/lovelace/views/hui-panel-view.ts index 72ddb347d7..83715e9a95 100644 --- a/src/panels/lovelace/views/hui-panel-view.ts +++ b/src/panels/lovelace/views/hui-panel-view.ts @@ -63,7 +63,7 @@ export class PanelView extends LitElement implements LovelaceViewElement { protected render(): TemplateResult { return html` ${this.cards!.length > 1 - ? html` + ? html` ${this.hass!.localize( "ui.panel.lovelace.editor.view.panel_mode.warning_multiple_cards" )} diff --git a/src/translations/en.json b/src/translations/en.json index 488c2f82a9..24ababb65d 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -72,14 +72,15 @@ }, "badge": { "entity": { - "not_found": "[%key:ui::card::tile::not_found%]" + "not_found": "[%key:ui::card::common::entity_not_found%]" } }, "card": { "common": { "turn_on": "Turn on", "turn_off": "Turn off", - "toggle": "Toggle" + "toggle": "Toggle", + "entity_not_found": "Entity not found" }, "alarm_control_panel": { "code": "Code", @@ -257,9 +258,6 @@ "finish": "finish" } }, - "tile": { - "not_found": "Entity not found" - }, "vacuum": { "actions": { "resume_cleaning": "Resume cleaning", @@ -1010,7 +1008,7 @@ "my_calendars": "My calendars", "create_calendar": "Create calendar", "today": "Today", - "event_retrieval_error": "Could not retrieve events for calendars:", + "event_retrieval_error": "Could not retrieve events for calendars", "event": { "add": "Add event", "delete": "Delete event", From 7fa697a7680e90fde51a569244706e25f4a24f23 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 26 May 2025 10:50:42 +0200 Subject: [PATCH 20/30] Add actions section and add domains to other section in area dashboard (#25558) * Add actions section and add domains to other section in area dashboard * Add timer and input button * Better grouping --- .../strategies/areas/area-view-strategy.ts | 24 ++++++++++++++-- .../areas/areas-overview-view-strategy.ts | 1 + .../areas/helpers/areas-strategy-helper.ts | 28 ++++++++++++++++++- src/translations/en.json | 3 +- 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/panels/lovelace/strategies/areas/area-view-strategy.ts b/src/panels/lovelace/strategies/areas/area-view-strategy.ts index c2c2597a9f..2fc8a0256d 100644 --- a/src/panels/lovelace/strategies/areas/area-view-strategy.ts +++ b/src/panels/lovelace/strategies/areas/area-view-strategy.ts @@ -76,8 +76,15 @@ export class AreaViewStrategy extends ReactiveElement { const computeTileCard = computeAreaTileCardConfig(hass, area.name, true); - const { lights, climate, covers, media_players, security, others } = - groupedEntities; + const { + lights, + climate, + covers, + media_players, + security, + actions, + others, + } = groupedEntities; if (lights.length > 0) { sections.push({ @@ -146,6 +153,19 @@ export class AreaViewStrategy extends ReactiveElement { }); } + if (actions.length > 0) { + sections.push({ + type: "grid", + cards: [ + computeHeadingCard( + hass.localize("ui.panel.lovelace.strategy.areas.groups.actions"), + AREA_STRATEGY_GROUP_ICONS.actions + ), + ...actions.map(computeTileCard), + ], + }); + } + if (others.length > 0) { sections.push({ type: "grid", diff --git a/src/panels/lovelace/strategies/areas/areas-overview-view-strategy.ts b/src/panels/lovelace/strategies/areas/areas-overview-view-strategy.ts index fde3545216..c8cda13888 100644 --- a/src/panels/lovelace/strategies/areas/areas-overview-view-strategy.ts +++ b/src/panels/lovelace/strategies/areas/areas-overview-view-strategy.ts @@ -54,6 +54,7 @@ export class AreasOverviewViewStrategy extends ReactiveElement { ...groups.climate, ...groups.media_players, ...groups.security, + ...groups.actions, ...groups.others, ]; diff --git a/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts b/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts index 3c64b21457..065393e258 100644 --- a/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts +++ b/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts @@ -22,6 +22,7 @@ export const AREA_STRATEGY_GROUPS = [ "covers", "media_players", "security", + "actions", "others", ] as const; @@ -31,6 +32,7 @@ export const AREA_STRATEGY_GROUP_ICONS = { covers: "mdi:blinds-horizontal", media_players: "mdi:multimedia", security: "mdi:security", + actions: "mdi:robot", others: "mdi:shape", }; @@ -121,6 +123,18 @@ export const getAreaGroupedEntities = ( entity_category: "none", }), ], + actions: [ + generateEntityFilter(hass, { + domain: ["script", "scene"], + area: area, + entity_category: "none", + }), + generateEntityFilter(hass, { + domain: ["automation"], + area: area, + entity_category: "none", + }), + ], others: [ generateEntityFilter(hass, { domain: "vacuum", @@ -138,7 +152,19 @@ export const getAreaGroupedEntities = ( entity_category: "none", }), generateEntityFilter(hass, { - domain: ["switch", "select", "input_boolean", "input_select"], + domain: ["switch", "button", "input_boolean", "input_button"], + area: area, + entity_category: "none", + }), + generateEntityFilter(hass, { + domain: [ + "select", + "number", + "input_select", + "input_number", + "counter", + "timer", + ], area: area, entity_category: "none", }), diff --git a/src/translations/en.json b/src/translations/en.json index 24ababb65d..725074c0ca 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -6571,8 +6571,9 @@ "lights": "Lights", "covers": "Covers", "climate": "Climate", - "media_players": "Entertainment", + "media_players": "Media players", "security": "Security", + "actions": "Actions", "others": "Others" } } From 18aaa44d2d34c30e08112d6e0abd0bb1e040293d Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 26 May 2025 12:26:06 +0200 Subject: [PATCH 21/30] Use constant for Home Assistant bluetooth node in graph (#25595) --- .../bluetooth-network-visualization.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-network-visualization.ts b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-network-visualization.ts index ef382f8359..ee4e2bb8fd 100644 --- a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-network-visualization.ts +++ b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-network-visualization.ts @@ -32,6 +32,9 @@ import { throttle } from "../../../../../common/util/throttle"; const UPDATE_THROTTLE_TIME = 10000; +const CORE_SOURCE_ID = "ha"; +const CORE_SOURCE_LABEL = "Home Assistant"; + @customElement("bluetooth-network-visualization") export class BluetoothNetworkVisualization extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -130,7 +133,7 @@ export class BluetoothNetworkVisualization extends LitElement { ): NetworkData => { const categories = [ { - name: this.hass.localize("ui.panel.config.bluetooth.core"), + name: CORE_SOURCE_LABEL, symbol: "roundRect", itemStyle: { color: colorVariables["primary-color"], @@ -160,8 +163,8 @@ export class BluetoothNetworkVisualization extends LitElement { ]; const nodes: NetworkNode[] = [ { - id: "ha", - name: this.hass.localize("ui.panel.config.bluetooth.core"), + id: CORE_SOURCE_ID, + name: CORE_SOURCE_LABEL, category: 0, value: 4, symbol: "roundRect", @@ -183,7 +186,7 @@ export class BluetoothNetworkVisualization extends LitElement { polarDistance: 0.25, }); links.push({ - source: "ha", + source: CORE_SOURCE_ID, target: scanner.source, value: 0, symbol: "none", @@ -234,8 +237,8 @@ export class BluetoothNetworkVisualization extends LitElement { ); private _getBluetoothDeviceName(id: string): string { - if (id === "ha") { - return this.hass.localize("ui.panel.config.bluetooth.core"); + if (id === CORE_SOURCE_ID) { + return CORE_SOURCE_LABEL; } if (this._sourceDevices[id]) { return ( @@ -262,7 +265,7 @@ export class BluetoothNetworkVisualization extends LitElement { const sourceName = this._getBluetoothDeviceName(source); const targetName = this._getBluetoothDeviceName(target); tooltipText = `${sourceName} → ${targetName}`; - if (source !== "ha") { + if (source !== CORE_SOURCE_ID) { tooltipText += ` ${this.hass.localize("ui.panel.config.bluetooth.rssi")}: ${value}`; } } else { From da8d43f5d16d97e06bea6b2a20d8560d02938249 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 26 May 2025 14:56:11 +0300 Subject: [PATCH 22/30] Allow href="data:..." in config flow step description (#25559) * Allow href="data:..." in config flow step description * Update src/dialogs/config-flow/show-dialog-config-flow.ts --- src/components/ha-markdown-element.ts | 5 ++ src/components/ha-markdown.ts | 4 ++ .../config-flow/show-dialog-config-flow.ts | 7 ++- src/resources/markdown-worker.ts | 56 +++++++++++-------- 4 files changed, 48 insertions(+), 24 deletions(-) diff --git a/src/components/ha-markdown-element.ts b/src/components/ha-markdown-element.ts index 1c642df869..cbc6b81755 100644 --- a/src/components/ha-markdown-element.ts +++ b/src/components/ha-markdown-element.ts @@ -26,6 +26,9 @@ class HaMarkdownElement extends ReactiveElement { @property({ attribute: "allow-svg", type: Boolean }) public allowSvg = false; + @property({ attribute: "allow-data-url", type: Boolean }) + public allowDataUrl = false; + @property({ type: Boolean }) public breaks = false; @property({ type: Boolean, attribute: "lazy-images" }) public lazyImages = @@ -66,6 +69,7 @@ class HaMarkdownElement extends ReactiveElement { return hash({ content: this.content, allowSvg: this.allowSvg, + allowDataUrl: this.allowDataUrl, breaks: this.breaks, }); } @@ -79,6 +83,7 @@ class HaMarkdownElement extends ReactiveElement { }, { allowSvg: this.allowSvg, + allowDataUrl: this.allowDataUrl, } ); diff --git a/src/components/ha-markdown.ts b/src/components/ha-markdown.ts index 5bc9c26b74..51d4fdd49e 100644 --- a/src/components/ha-markdown.ts +++ b/src/components/ha-markdown.ts @@ -8,6 +8,9 @@ export class HaMarkdown extends LitElement { @property({ attribute: "allow-svg", type: Boolean }) public allowSvg = false; + @property({ attribute: "allow-data-url", type: Boolean }) + public allowDataUrl = false; + @property({ type: Boolean }) public breaks = false; @property({ type: Boolean, attribute: "lazy-images" }) public lazyImages = @@ -23,6 +26,7 @@ export class HaMarkdown extends LitElement { return html` + ` : ""; }, diff --git a/src/resources/markdown-worker.ts b/src/resources/markdown-worker.ts index 1a8d3e3dc1..ae8c5a3a68 100644 --- a/src/resources/markdown-worker.ts +++ b/src/resources/markdown-worker.ts @@ -7,34 +7,13 @@ import { filterXSS, getDefaultWhiteList } from "xss"; let whiteListNormal: IWhiteList | undefined; let whiteListSvg: IWhiteList | undefined; -// Override the default `onTagAttr` behavior to only render -// our markdown checkboxes. -// Returning undefined causes the default measure to be taken -// in the xss library. -const onTagAttr = ( - tag: string, - name: string, - value: string -): string | undefined => { - if (tag === "input") { - if ( - (name === "type" && value === "checkbox") || - name === "checked" || - name === "disabled" - ) { - return undefined; - } - return ""; - } - return undefined; -}; - const renderMarkdown = async ( content: string, markedOptions: MarkedOptions, hassOptions: { // Do not allow SVG on untrusted content, it allows XSS. allowSvg?: boolean; + allowDataUrl?: boolean; } = {} ): Promise => { if (!whiteListNormal) { @@ -70,10 +49,41 @@ const renderMarkdown = async ( } else { whiteList = whiteListNormal; } + if (hassOptions.allowDataUrl && whiteList.a) { + whiteList.a.push("download"); + } return filterXSS(await marked(content, markedOptions), { whiteList, - onTagAttr, + onTagAttr: ( + tag: string, + name: string, + value: string + ): string | undefined => { + // Override the default `onTagAttr` behavior to only render + // our markdown checkboxes. + // Returning undefined causes the default measure to be taken + // in the xss library. + if (tag === "input") { + if ( + (name === "type" && value === "checkbox") || + name === "checked" || + name === "disabled" + ) { + return undefined; + } + return ""; + } + if ( + hassOptions.allowDataUrl && + tag === "a" && + name === "href" && + value.startsWith("data:") + ) { + return `href="${value}"`; + } + return undefined; + }, }); }; From 02b4b8e334774f436d49a367d187da5d147f5da5 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Mon, 26 May 2025 05:19:09 -0700 Subject: [PATCH 23/30] Fix device actions in step-flow-form (#24539) * Fix device actions in step-flow-form * Move xlation fetch to device action * also conditions & triggers * move to firstUpdated --- .../device/ha-device-automation-picker.ts | 2 + .../ha-selector/ha-selector-action.ts | 37 ++++++++++++++++++- .../types/ha-automation-action-device_id.ts | 5 ++- .../types/ha-automation-condition-device.ts | 5 ++- .../types/ha-automation-trigger-device.ts | 1 + 5 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/components/device/ha-device-automation-picker.ts b/src/components/device/ha-device-automation-picker.ts index ea3da6fba4..9a424d82fc 100644 --- a/src/components/device/ha-device-automation-picker.ts +++ b/src/components/device/ha-device-automation-picker.ts @@ -12,6 +12,7 @@ import type { EntityRegistryEntry } from "../../data/entity_registry"; import type { HomeAssistant } from "../../types"; import "../ha-list-item"; import "../ha-select"; +import { stopPropagation } from "../../common/dom/stop_propagation"; const NO_AUTOMATION_KEY = "NO_AUTOMATION"; const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION"; @@ -103,6 +104,7 @@ export abstract class HaDeviceAutomationPicker< .label=${this.label} .value=${value} @selected=${this._automationChanged} + @closed=${stopPropagation} .disabled=${this._automations.length === 0} > ${value === NO_AUTOMATION_KEY diff --git a/src/components/ha-selector/ha-selector-action.ts b/src/components/ha-selector/ha-selector-action.ts index a3d94b9742..d6f531544c 100644 --- a/src/components/ha-selector/ha-selector-action.ts +++ b/src/components/ha-selector/ha-selector-action.ts @@ -1,14 +1,22 @@ +import { ContextProvider, consume } from "@lit-labs/context"; import { css, html, LitElement, nothing } from "lit"; -import { customElement, property } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; +import type { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { fullEntitiesContext } from "../../data/context"; import type { Action } from "../../data/script"; import { migrateAutomationAction } from "../../data/script"; import type { ActionSelector } from "../../data/selector"; import "../../panels/config/automation/action/ha-automation-action"; import type { HomeAssistant } from "../../types"; +import { + subscribeEntityRegistry, + type EntityRegistryEntry, +} from "../../data/entity_registry"; +import { SubscribeMixin } from "../../mixins/subscribe-mixin"; @customElement("ha-selector-action") -export class HaActionSelector extends LitElement { +export class HaActionSelector extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public selector!: ActionSelector; @@ -19,6 +27,14 @@ export class HaActionSelector extends LitElement { @property({ type: Boolean, reflect: true }) public disabled = false; + @state() + @consume({ context: fullEntitiesContext, subscribe: true }) + _entityReg: EntityRegistryEntry[] | undefined; + + @state() private _entitiesContext; + + protected hassSubscribeRequiredHostProps = ["_entitiesContext"]; + private _actions = memoizeOne((action: Action | undefined) => { if (!action) { return []; @@ -26,6 +42,23 @@ export class HaActionSelector extends LitElement { return migrateAutomationAction(action); }); + protected firstUpdated() { + if (!this._entityReg) { + this._entitiesContext = new ContextProvider(this, { + context: fullEntitiesContext, + initialValue: [], + }); + } + } + + public hassSubscribe(): UnsubscribeFunc[] { + return [ + subscribeEntityRegistry(this.hass.connection!, (entities) => { + this._entitiesContext.setValue(entities); + }), + ]; + } + protected render() { return html` ${this.label ? html`` : nothing} diff --git a/src/panels/config/automation/action/types/ha-automation-action-device_id.ts b/src/panels/config/automation/action/types/ha-automation-action-device_id.ts index 0cc01e80c6..a1637cccfe 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-device_id.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-device_id.ts @@ -127,6 +127,7 @@ export class HaDeviceAction extends LitElement { } protected firstUpdated() { + this.hass.loadBackendTranslation("device_automation"); if (!this._capabilities) { this._getCapabilities(); } @@ -135,8 +136,8 @@ export class HaDeviceAction extends LitElement { } } - protected updated(changedPros) { - const prevAction = changedPros.get("action"); + protected updated(changedProps) { + const prevAction = changedProps.get("action"); if ( prevAction && !deviceAutomationsEqual(this._entityReg, prevAction, this.action) diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-device.ts b/src/panels/config/automation/condition/types/ha-automation-condition-device.ts index 7b03f5164d..c6cea685f3 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-device.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-device.ts @@ -128,6 +128,7 @@ export class HaDeviceCondition extends LitElement { } protected firstUpdated() { + this.hass.loadBackendTranslation("device_automation"); if (!this._capabilities) { this._getCapabilities(); } @@ -136,8 +137,8 @@ export class HaDeviceCondition extends LitElement { } } - protected updated(changedPros) { - const prevCondition = changedPros.get("condition"); + protected updated(changedProps) { + const prevCondition = changedProps.get("condition"); if ( prevCondition && !deviceAutomationsEqual(this._entityReg, prevCondition, this.condition) diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-device.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-device.ts index d329bd5221..238454a8a2 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-device.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-device.ts @@ -132,6 +132,7 @@ export class HaDeviceTrigger extends LitElement { } protected firstUpdated() { + this.hass.loadBackendTranslation("device_automation"); if (!this._capabilities) { this._getCapabilities(); } From 3e5bd64b8331c694409eb23e055fd8458207ee80 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 26 May 2025 15:14:59 +0200 Subject: [PATCH 24/30] Update ha-selector-action.ts --- src/components/ha-selector/ha-selector-action.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ha-selector/ha-selector-action.ts b/src/components/ha-selector/ha-selector-action.ts index d6f531544c..290e306383 100644 --- a/src/components/ha-selector/ha-selector-action.ts +++ b/src/components/ha-selector/ha-selector-action.ts @@ -1,4 +1,4 @@ -import { ContextProvider, consume } from "@lit-labs/context"; +import { ContextProvider, consume } from "@lit/context"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; From 114c1fb98b127819fb7a31e4146ca1d451f2fc7c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 15:20:08 +0200 Subject: [PATCH 25/30] Update vaadinWebComponents monorepo to v24.7.7 (#25596) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 +- yarn.lock | 170 +++++++++++++++++++++++++-------------------------- 2 files changed, 87 insertions(+), 87 deletions(-) diff --git a/package.json b/package.json index 28ef50db6e..5f9120e037 100644 --- a/package.json +++ b/package.json @@ -89,8 +89,8 @@ "@thomasloven/round-slider": "0.6.0", "@tsparticles/engine": "3.8.1", "@tsparticles/preset-links": "3.2.0", - "@vaadin/combo-box": "24.7.6", - "@vaadin/vaadin-themable-mixin": "24.7.6", + "@vaadin/combo-box": "24.7.7", + "@vaadin/vaadin-themable-mixin": "24.7.7", "@vibrant/color": "4.0.0", "@vue/web-component-wrapper": "1.3.0", "@webcomponents/scoped-custom-element-registry": "0.0.10", diff --git a/yarn.lock b/yarn.lock index 816ff832ff..feae95c297 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4996,131 +4996,131 @@ __metadata: languageName: node linkType: hard -"@vaadin/a11y-base@npm:~24.7.6": - version: 24.7.6 - resolution: "@vaadin/a11y-base@npm:24.7.6" +"@vaadin/a11y-base@npm:~24.7.7": + version: 24.7.7 + resolution: "@vaadin/a11y-base@npm:24.7.7" dependencies: "@open-wc/dedupe-mixin": "npm:^1.3.0" "@polymer/polymer": "npm:^3.0.0" - "@vaadin/component-base": "npm:~24.7.6" + "@vaadin/component-base": "npm:~24.7.7" lit: "npm:^3.0.0" - checksum: 10/bfe70e26a1f6cf01812aa54c889d8a767d86092ae787f883af4836d13cd6a9b9eb04d1686eb0acf2c7cc40dde597a6284ad5baf0cfe280cfb70ceb9868bfbcfe + checksum: 10/4fbd7eb641b096af67b7111e745bcf45e9a60ee0182a8dcb4a86ddd9dbec4f7e444d93bb3e5cd513fd6c4a10002d0862ae47b286a568d319d01492726a2ffbfd languageName: node linkType: hard -"@vaadin/combo-box@npm:24.7.6": - version: 24.7.6 - resolution: "@vaadin/combo-box@npm:24.7.6" +"@vaadin/combo-box@npm:24.7.7": + version: 24.7.7 + resolution: "@vaadin/combo-box@npm:24.7.7" dependencies: "@open-wc/dedupe-mixin": "npm:^1.3.0" "@polymer/polymer": "npm:^3.0.0" - "@vaadin/a11y-base": "npm:~24.7.6" - "@vaadin/component-base": "npm:~24.7.6" - "@vaadin/field-base": "npm:~24.7.6" - "@vaadin/input-container": "npm:~24.7.6" - "@vaadin/item": "npm:~24.7.6" - "@vaadin/lit-renderer": "npm:~24.7.6" - "@vaadin/overlay": "npm:~24.7.6" - "@vaadin/vaadin-lumo-styles": "npm:~24.7.6" - "@vaadin/vaadin-material-styles": "npm:~24.7.6" - "@vaadin/vaadin-themable-mixin": "npm:~24.7.6" + "@vaadin/a11y-base": "npm:~24.7.7" + "@vaadin/component-base": "npm:~24.7.7" + "@vaadin/field-base": "npm:~24.7.7" + "@vaadin/input-container": "npm:~24.7.7" + "@vaadin/item": "npm:~24.7.7" + "@vaadin/lit-renderer": "npm:~24.7.7" + "@vaadin/overlay": "npm:~24.7.7" + "@vaadin/vaadin-lumo-styles": "npm:~24.7.7" + "@vaadin/vaadin-material-styles": "npm:~24.7.7" + "@vaadin/vaadin-themable-mixin": "npm:~24.7.7" lit: "npm:^3.0.0" - checksum: 10/0621220bb50c5f8e320fffb1254085b58562bddfa2c1739241eae6a336b86cdbcd1ba0bf091281dbbf74e01d375e174259cc61a1cc1fedba97df21aa709814a7 + checksum: 10/ac9af96785d03ede12ac85c6a00f94a2122fc85db168ca2669a716138bc99a7df29630d4ede3274a6a9e97b53eac074c2f86480be0c3468d1b0118a4aca2aecd languageName: node linkType: hard -"@vaadin/component-base@npm:~24.7.6": - version: 24.7.6 - resolution: "@vaadin/component-base@npm:24.7.6" +"@vaadin/component-base@npm:~24.7.7": + version: 24.7.7 + resolution: "@vaadin/component-base@npm:24.7.7" dependencies: "@open-wc/dedupe-mixin": "npm:^1.3.0" "@polymer/polymer": "npm:^3.0.0" "@vaadin/vaadin-development-mode-detector": "npm:^2.0.0" "@vaadin/vaadin-usage-statistics": "npm:^2.1.0" lit: "npm:^3.0.0" - checksum: 10/deec240a9b1520b5fbba8c749a8ad258bfe776480d0159ec80fe5699c32e1dd0f80fe8a6e900866fed43eaf5ee5ec5f811949a260d195288941f19d9d2572fb4 + checksum: 10/ac8793e1feebdd4a3f063b5715f0110c8a2ec64d869c45aa753ce4bbe6d311721741e8363648a76fbe02b1d4c8a5099bc65aeb2dfada0e042d02d7630eaa6204 languageName: node linkType: hard -"@vaadin/field-base@npm:~24.7.6": - version: 24.7.6 - resolution: "@vaadin/field-base@npm:24.7.6" +"@vaadin/field-base@npm:~24.7.7": + version: 24.7.7 + resolution: "@vaadin/field-base@npm:24.7.7" dependencies: "@open-wc/dedupe-mixin": "npm:^1.3.0" "@polymer/polymer": "npm:^3.0.0" - "@vaadin/a11y-base": "npm:~24.7.6" - "@vaadin/component-base": "npm:~24.7.6" + "@vaadin/a11y-base": "npm:~24.7.7" + "@vaadin/component-base": "npm:~24.7.7" lit: "npm:^3.0.0" - checksum: 10/bd5ee1194f35fee660fa20bfca0096b1078da63602fd7bb2e49e34361bcf33ddc509b40ec5426015b0e5835ae1c168335ffd7542c7c8a611100ac3572240d7b0 + checksum: 10/9e8a4f6235ff0296d0258e4d8f6514ccaab19a06c06d9f803a3cba9f238307029135f06cd37fe865f1cb7acc78383df935010424c76e7d1492eed74031fa74ba languageName: node linkType: hard -"@vaadin/icon@npm:~24.7.6": - version: 24.7.6 - resolution: "@vaadin/icon@npm:24.7.6" +"@vaadin/icon@npm:~24.7.7": + version: 24.7.7 + resolution: "@vaadin/icon@npm:24.7.7" dependencies: "@open-wc/dedupe-mixin": "npm:^1.3.0" "@polymer/polymer": "npm:^3.0.0" - "@vaadin/component-base": "npm:~24.7.6" - "@vaadin/vaadin-lumo-styles": "npm:~24.7.6" - "@vaadin/vaadin-themable-mixin": "npm:~24.7.6" + "@vaadin/component-base": "npm:~24.7.7" + "@vaadin/vaadin-lumo-styles": "npm:~24.7.7" + "@vaadin/vaadin-themable-mixin": "npm:~24.7.7" lit: "npm:^3.0.0" - checksum: 10/6ed88de3634c51fa268a71a4d762c908f85dd5a8748cd0a27c6c5a0b715a115d4fef2c6cb046e544d93fdd8054748204cd661b5608afe15666ed3d30d50c47ed + checksum: 10/5313651ae4508e2cd554d2af242cf8c390c75ccdb12c40be0ec0703fee20d058d93f4a58a14ebacd2b867702acd5467206316b7cdf6aa538494b29deb72d220f languageName: node linkType: hard -"@vaadin/input-container@npm:~24.7.6": - version: 24.7.6 - resolution: "@vaadin/input-container@npm:24.7.6" +"@vaadin/input-container@npm:~24.7.7": + version: 24.7.7 + resolution: "@vaadin/input-container@npm:24.7.7" dependencies: "@polymer/polymer": "npm:^3.0.0" - "@vaadin/component-base": "npm:~24.7.6" - "@vaadin/vaadin-lumo-styles": "npm:~24.7.6" - "@vaadin/vaadin-material-styles": "npm:~24.7.6" - "@vaadin/vaadin-themable-mixin": "npm:~24.7.6" + "@vaadin/component-base": "npm:~24.7.7" + "@vaadin/vaadin-lumo-styles": "npm:~24.7.7" + "@vaadin/vaadin-material-styles": "npm:~24.7.7" + "@vaadin/vaadin-themable-mixin": "npm:~24.7.7" lit: "npm:^3.0.0" - checksum: 10/96a46f32a29ad6ee0478136875af65a8d5f5319d1cbf40185d271088d6385ae5e25136c6e9b1db5873a54fd705d133088d5f8b35564cd1d0db7bda0a4b6ec3d3 + checksum: 10/f180e8dd6544f6f86c860a5b60c90a7f33342368b499e60fab7d136cbd2f1294705c97c1211ca284bcfb5749ae4b043d14ce6e8d2e388c5a494a2c0d63afd3d5 languageName: node linkType: hard -"@vaadin/item@npm:~24.7.6": - version: 24.7.6 - resolution: "@vaadin/item@npm:24.7.6" +"@vaadin/item@npm:~24.7.7": + version: 24.7.7 + resolution: "@vaadin/item@npm:24.7.7" dependencies: "@open-wc/dedupe-mixin": "npm:^1.3.0" "@polymer/polymer": "npm:^3.0.0" - "@vaadin/a11y-base": "npm:~24.7.6" - "@vaadin/component-base": "npm:~24.7.6" - "@vaadin/vaadin-lumo-styles": "npm:~24.7.6" - "@vaadin/vaadin-material-styles": "npm:~24.7.6" - "@vaadin/vaadin-themable-mixin": "npm:~24.7.6" + "@vaadin/a11y-base": "npm:~24.7.7" + "@vaadin/component-base": "npm:~24.7.7" + "@vaadin/vaadin-lumo-styles": "npm:~24.7.7" + "@vaadin/vaadin-material-styles": "npm:~24.7.7" + "@vaadin/vaadin-themable-mixin": "npm:~24.7.7" lit: "npm:^3.0.0" - checksum: 10/4ca34fcd781d1a222ff603b4673dc15cfcd574280227ce5fd783e981928993fade795f432807ee8afb0e81105e85309ab0dcc0bebbf25ae786f6c850b71739d2 + checksum: 10/12db749e653e7c696844ba254c4beb10ec2ccc0b631c0e797d1a97fe72baadfa9c2d87ba890da2b5a7471c6dee2054d253615fd450e4fe8bff4091a8a4558b87 languageName: node linkType: hard -"@vaadin/lit-renderer@npm:~24.7.6": - version: 24.7.6 - resolution: "@vaadin/lit-renderer@npm:24.7.6" +"@vaadin/lit-renderer@npm:~24.7.7": + version: 24.7.7 + resolution: "@vaadin/lit-renderer@npm:24.7.7" dependencies: lit: "npm:^3.0.0" - checksum: 10/82bf563f6712786152698447af478918354269b35bc426964d46e200424f1403f9c4716b2408146d8c6e796b4d6fd307f18e749a48359d95a52a7c091261d90f + checksum: 10/9b8a34e9f71ac292ea2e8bef9db282c923e532c24f6179df507445d9cfdd49494d03ac70ec0d215abc9b4d83e23b5ff087f8c8ebac146ac8a3ada490e65337ab languageName: node linkType: hard -"@vaadin/overlay@npm:~24.7.6": - version: 24.7.6 - resolution: "@vaadin/overlay@npm:24.7.6" +"@vaadin/overlay@npm:~24.7.7": + version: 24.7.7 + resolution: "@vaadin/overlay@npm:24.7.7" dependencies: "@open-wc/dedupe-mixin": "npm:^1.3.0" "@polymer/polymer": "npm:^3.0.0" - "@vaadin/a11y-base": "npm:~24.7.6" - "@vaadin/component-base": "npm:~24.7.6" - "@vaadin/vaadin-lumo-styles": "npm:~24.7.6" - "@vaadin/vaadin-material-styles": "npm:~24.7.6" - "@vaadin/vaadin-themable-mixin": "npm:~24.7.6" + "@vaadin/a11y-base": "npm:~24.7.7" + "@vaadin/component-base": "npm:~24.7.7" + "@vaadin/vaadin-lumo-styles": "npm:~24.7.7" + "@vaadin/vaadin-material-styles": "npm:~24.7.7" + "@vaadin/vaadin-themable-mixin": "npm:~24.7.7" lit: "npm:^3.0.0" - checksum: 10/a0b4e6e1f3b32270b201b993f34214580815121a9ec8727d85536f692df4d3713477fa4463c3f8d515afdd418e2c03cbac62256fc00e7cba9acd627295269154 + checksum: 10/b1b32c31a0edb2ac73a3b2c260a1f9b57e078fd29329b8c26ff20f1a51ba66ad6ee6a3e4cd6e871313270656fc7ccf2d7f7beb77892e066265eb8cf013cc67c4 languageName: node linkType: hard @@ -5131,36 +5131,36 @@ __metadata: languageName: node linkType: hard -"@vaadin/vaadin-lumo-styles@npm:~24.7.6": - version: 24.7.6 - resolution: "@vaadin/vaadin-lumo-styles@npm:24.7.6" +"@vaadin/vaadin-lumo-styles@npm:~24.7.7": + version: 24.7.7 + resolution: "@vaadin/vaadin-lumo-styles@npm:24.7.7" dependencies: "@polymer/polymer": "npm:^3.0.0" - "@vaadin/component-base": "npm:~24.7.6" - "@vaadin/icon": "npm:~24.7.6" - "@vaadin/vaadin-themable-mixin": "npm:~24.7.6" - checksum: 10/97e59f3952eb372e7c11375e4714e1d7cff77aacbd941b16282da5cabc7a84d514e530470b330a40a6afcc3a3984e8d815d61287ebcee4d8bff15d616ceb8cbb + "@vaadin/component-base": "npm:~24.7.7" + "@vaadin/icon": "npm:~24.7.7" + "@vaadin/vaadin-themable-mixin": "npm:~24.7.7" + checksum: 10/d4719841f16c4c4ff3c31dbe57bd3a6b8e2ccdfe0189490d84e48bf5ee50ea3f1c741b5c0478598c01603eb696007a2e877f93560bf22b0d875394f06235bb6d languageName: node linkType: hard -"@vaadin/vaadin-material-styles@npm:~24.7.6": - version: 24.7.6 - resolution: "@vaadin/vaadin-material-styles@npm:24.7.6" +"@vaadin/vaadin-material-styles@npm:~24.7.7": + version: 24.7.7 + resolution: "@vaadin/vaadin-material-styles@npm:24.7.7" dependencies: "@polymer/polymer": "npm:^3.0.0" - "@vaadin/component-base": "npm:~24.7.6" - "@vaadin/vaadin-themable-mixin": "npm:~24.7.6" - checksum: 10/0b58f7b09b9144d6895c6b33f3db495d8cc30dd60f887a64f4b10b00d5acb99ea712d78a44c880233bfa56b361ad9f4af8e7319738ea53b6c20ad0bff44fe4e2 + "@vaadin/component-base": "npm:~24.7.7" + "@vaadin/vaadin-themable-mixin": "npm:~24.7.7" + checksum: 10/ef0588a3d7515434639d9d3f5b9bfa672e4ee4ef7577202c0e8b315462895e840d9abbb18efb28f71e39f31aeb8b08f63ad049960081def8fb790db65e54f65a languageName: node linkType: hard -"@vaadin/vaadin-themable-mixin@npm:24.7.6, @vaadin/vaadin-themable-mixin@npm:~24.7.6": - version: 24.7.6 - resolution: "@vaadin/vaadin-themable-mixin@npm:24.7.6" +"@vaadin/vaadin-themable-mixin@npm:24.7.7, @vaadin/vaadin-themable-mixin@npm:~24.7.7": + version: 24.7.7 + resolution: "@vaadin/vaadin-themable-mixin@npm:24.7.7" dependencies: "@open-wc/dedupe-mixin": "npm:^1.3.0" lit: "npm:^3.0.0" - checksum: 10/40a498c0ed0657cd1cc6cc8f414d37eb8894066b91a9eb3f6de7d7e719bc0a84384ded490eb02aef89c5fd28902fb0e33888f22cb3b48a1c38d63743c6899218 + checksum: 10/c4905df956baf4255029cf61f02366db61822e0db664baf279daf075e9122eb51bc85f0527f12f1bc004c279ab404c5b6f765a9d581ba729463165b6e2e07dd0 languageName: node linkType: hard @@ -9348,8 +9348,8 @@ __metadata: "@types/tar": "npm:6.1.13" "@types/ua-parser-js": "npm:0.7.39" "@types/webspeechapi": "npm:0.0.29" - "@vaadin/combo-box": "npm:24.7.6" - "@vaadin/vaadin-themable-mixin": "npm:24.7.6" + "@vaadin/combo-box": "npm:24.7.7" + "@vaadin/vaadin-themable-mixin": "npm:24.7.7" "@vibrant/color": "npm:4.0.0" "@vitest/coverage-v8": "npm:3.1.4" "@vue/web-component-wrapper": "npm:1.3.0" From 9f5f100e9832766b35e9839073bd2d871f98f480 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Mon, 26 May 2025 06:24:10 -0700 Subject: [PATCH 26/30] Tweak rules for entity-filter card (#25570) Co-authored-by: Bram Kragten --- .../lovelace/cards/hui-entity-filter-card.ts | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/panels/lovelace/cards/hui-entity-filter-card.ts b/src/panels/lovelace/cards/hui-entity-filter-card.ts index 96239b8538..5e8ed06d6f 100644 --- a/src/panels/lovelace/cards/hui-entity-filter-card.ts +++ b/src/panels/lovelace/cards/hui-entity-filter-card.ts @@ -82,18 +82,40 @@ export class HuiEntityFilterCard } if ( - !( - (config.conditions && Array.isArray(config.conditions)) || - (config.state_filter && Array.isArray(config.state_filter)) - ) && - !config.entities.every( + !config.conditions && + !config.state_filter && + !config.entities.some( (entity) => typeof entity === "object" && - entity.state_filter && - Array.isArray(entity.state_filter) + (entity.state_filter || entity.conditions) ) ) { - throw new Error("Incorrect filter config"); + throw new Error("At least one conditions or state_filter is required"); + } + + if ( + (config.conditions && !Array.isArray(config.conditions)) || + (config.state_filter && !Array.isArray(config.state_filter)) || + config.entities.some( + (entity) => + typeof entity === "object" && + ((entity.state_filter && !Array.isArray(entity.state_filter)) || + (entity.conditions && !Array.isArray(entity.conditions))) + ) + ) { + throw new Error("Conditions or state_filter must be an array"); + } + + if ( + (config.conditions && config.state_filter) || + config.entities.some( + (entity) => + typeof entity === "object" && entity.state_filter && entity.conditions + ) + ) { + throw new Error( + "Conditions and state_filter may not be simultaneously defined" + ); } this._configEntities = processConfigEntities(config.entities); @@ -149,7 +171,7 @@ export class HuiEntityFilterCard if (!stateObj) return false; const conditions = entityConf.conditions ?? this._config!.conditions; - if (conditions) { + if (conditions && !entityConf.state_filter) { const conditionWithEntity = conditions.map((condition) => addEntityToCondition(condition, entityConf.entity) ); @@ -161,7 +183,7 @@ export class HuiEntityFilterCard return filters.some((filter) => evaluateStateFilter(stateObj, filter)); } - return false; + return true; }); if (entitiesList.length === 0 && this._config.show_empty === false) { From 208e863327af1d7769856315f8e4a9a2ddbe3702 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Mon, 26 May 2025 15:25:11 +0200 Subject: [PATCH 27/30] Save sidebar in user data (#25555) Co-authored-by: Petar Petrov Co-authored-by: Bram Kragten --- src/components/ha-sidebar.ts | 126 ++++++++-------- src/data/frontend.ts | 6 + src/data/translation.ts | 9 +- src/dialogs/sidebar/dialog-edit-sidebar.ts | 140 +++++++++++++----- .../sidebar/show-dialog-edit-sidebar.ts | 11 +- src/layouts/home-assistant-main.ts | 36 +---- .../profile/ha-profile-section-general.ts | 45 +++--- src/translations/en.json | 4 +- 8 files changed, 203 insertions(+), 174 deletions(-) diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 64476ae81a..d2f77a9770 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -14,7 +14,6 @@ import { mdiTooltipAccount, mdiViewDashboard, } from "@mdi/js"; -import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { CSSResultGroup, PropertyValues } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { @@ -30,6 +29,7 @@ import { fireEvent } from "../common/dom/fire_event"; import { toggleAttribute } from "../common/dom/toggle_attribute"; import { stringCompare } from "../common/string/compare"; import { throttle } from "../common/util/throttle"; +import { subscribeFrontendUserData } from "../data/frontend"; import type { ActionHandlerDetail } from "../data/lovelace/action_handler"; import type { PersistentNotification } from "../data/persistent_notification"; import { subscribeNotifications } from "../data/persistent_notification"; @@ -41,11 +41,13 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import { haStyleScrollbar } from "../resources/styles"; import type { HomeAssistant, PanelInfo, Route } from "../types"; +import "./ha-fade-in"; import "./ha-icon"; import "./ha-icon-button"; import "./ha-md-list"; import "./ha-md-list-item"; import type { HaMdListItem } from "./ha-md-list-item"; +import "./ha-spinner"; import "./ha-svg-icon"; import "./user/ha-user-badge"; @@ -187,38 +189,57 @@ class HaSidebar extends SubscribeMixin(LitElement) { @property({ attribute: "always-expand", type: Boolean }) public alwaysExpand = false; - @property({ attribute: false }) - public panelOrder!: string[]; - - @property({ attribute: false }) - public hiddenPanels!: string[]; - @state() private _notifications?: PersistentNotification[]; @state() private _updatesCount = 0; @state() private _issuesCount = 0; + @state() private _panelOrder?: string[]; + + @state() private _hiddenPanels?: string[]; + private _mouseLeaveTimeout?: number; private _tooltipHideTimeout?: number; private _recentKeydownActiveUntil = 0; - private _unsubPersistentNotifications: UnsubscribeFunc | undefined; - @query(".tooltip") private _tooltip!: HTMLDivElement; - public hassSubscribe(): UnsubscribeFunc[] { - return this.hass.user?.is_admin - ? [ - subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => { - this._issuesCount = repairs.issues.filter( - (issue) => !issue.ignored - ).length; - }), - ] - : []; + public hassSubscribe() { + return [ + subscribeFrontendUserData( + this.hass.connection, + "sidebar", + ({ value }) => { + this._panelOrder = value?.panelOrder; + this._hiddenPanels = value?.hiddenPanels; + + // fallback to old localStorage values + if (!this._panelOrder) { + const storedOrder = localStorage.getItem("sidebarPanelOrder"); + this._panelOrder = storedOrder ? JSON.parse(storedOrder) : []; + } + if (!this._hiddenPanels) { + const storedHidden = localStorage.getItem("sidebarHiddenPanels"); + this._hiddenPanels = storedHidden ? JSON.parse(storedHidden) : []; + } + } + ), + subscribeNotifications(this.hass.connection, (notifications) => { + this._notifications = notifications; + }), + ...(this.hass.user?.is_admin + ? [ + subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => { + this._issuesCount = repairs.issues.filter( + (issue) => !issue.ignored + ).length; + }), + ] + : []), + ]; } protected render() { @@ -254,8 +275,8 @@ class HaSidebar extends SubscribeMixin(LitElement) { changedProps.has("_updatesCount") || changedProps.has("_issuesCount") || changedProps.has("_notifications") || - changedProps.has("hiddenPanels") || - changedProps.has("panelOrder") + changedProps.has("_hiddenPanels") || + changedProps.has("_panelOrder") ) { return true; } @@ -279,23 +300,6 @@ class HaSidebar extends SubscribeMixin(LitElement) { ); } - protected firstUpdated(changedProps: PropertyValues) { - super.firstUpdated(changedProps); - this._subscribePersistentNotifications(); - } - - private _subscribePersistentNotifications(): void { - if (this._unsubPersistentNotifications) { - this._unsubPersistentNotifications(); - } - this._unsubPersistentNotifications = subscribeNotifications( - this.hass.connection, - (notifications) => { - this._notifications = notifications; - } - ); - } - protected updated(changedProps) { super.updated(changedProps); if (changedProps.has("alwaysExpand")) { @@ -307,14 +311,6 @@ class HaSidebar extends SubscribeMixin(LitElement) { const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if ( - this.hass && - oldHass?.connected === false && - this.hass.connected === true - ) { - this._subscribePersistentNotifications(); - } - this._calculateCounts(); if (!SUPPORT_SCROLL_IF_NEEDED) { @@ -369,11 +365,19 @@ class HaSidebar extends SubscribeMixin(LitElement) { } private _renderAllPanels(selectedPanel: string) { + if (!this._panelOrder || !this._hiddenPanels) { + return html` + + `; + } + const [beforeSpacer, afterSpacer] = computePanels( this.hass.panels, this.hass.defaultPanel, - this.panelOrder, - this.hiddenPanels, + this._panelOrder, + this._hiddenPanels, this.hass.locale ); @@ -559,18 +563,9 @@ class HaSidebar extends SubscribeMixin(LitElement) { return; } - showEditSidebarDialog(this, { - saveCallback: this._saveSidebar, - }); + showEditSidebarDialog(this); } - private _saveSidebar = (order: string[], hidden: string[]) => { - fireEvent(this, "hass-edit-sidebar", { - order, - hidden, - }); - }; - private _itemMouseEnter(ev: MouseEvent) { // On keypresses on the listbox, we're going to ignore mouse enter events // for 100ms so that we ignore it when pressing down arrow scrolls the @@ -730,13 +725,22 @@ class HaSidebar extends SubscribeMixin(LitElement) { display: none; } + ha-fade-in, ha-md-list { - padding: 4px 0; - box-sizing: border-box; - height: calc(100% - var(--header-height) - 132px); height: calc( 100% - var(--header-height) - 132px - var(--safe-area-inset-bottom) ); + } + + ha-fade-in { + display: flex; + justify-content: center; + align-items: center; + } + + ha-md-list { + padding: 4px 0; + box-sizing: border-box; overflow-x: hidden; background: none; margin-left: var(--safe-area-inset-left); diff --git a/src/data/frontend.ts b/src/data/frontend.ts index 7e46217cca..8f0c6749cc 100644 --- a/src/data/frontend.ts +++ b/src/data/frontend.ts @@ -5,9 +5,15 @@ export interface CoreFrontendUserData { showEntityIdPicker?: boolean; } +export interface SidebarFrontendUserData { + panelOrder: string[]; + hiddenPanels: string[]; +} + declare global { interface FrontendUserData { core: CoreFrontendUserData; + sidebar: SidebarFrontendUserData; } } diff --git a/src/data/translation.ts b/src/data/translation.ts index 260294e506..88ffff8ee8 100644 --- a/src/data/translation.ts +++ b/src/data/translation.ts @@ -1,9 +1,5 @@ import type { HomeAssistant } from "../types"; -import { - fetchFrontendUserData, - saveFrontendUserData, - subscribeFrontendUserData, -} from "./frontend"; +import { saveFrontendUserData, subscribeFrontendUserData } from "./frontend"; export enum NumberFormat { language = "language", @@ -78,9 +74,6 @@ export type TranslationCategory = | "selector" | "services"; -export const fetchTranslationPreferences = (hass: HomeAssistant) => - fetchFrontendUserData(hass.connection, "language"); - export const subscribeTranslationPreferences = ( hass: HomeAssistant, callback: (data: { value: FrontendLocaleData | null }) => void diff --git a/src/dialogs/sidebar/dialog-edit-sidebar.ts b/src/dialogs/sidebar/dialog-edit-sidebar.ts index efc42284cc..fed10ad593 100644 --- a/src/dialogs/sidebar/dialog-edit-sidebar.ts +++ b/src/dialogs/sidebar/dialog-edit-sidebar.ts @@ -1,18 +1,25 @@ import "@material/mwc-linear-progress/mwc-linear-progress"; import { mdiClose } from "@mdi/js"; -import { css, html, LitElement, nothing } from "lit"; +import { css, html, LitElement, nothing, type TemplateResult } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../common/dom/fire_event"; +import "../../components/ha-alert"; import "../../components/ha-dialog-header"; +import "../../components/ha-fade-in"; import "../../components/ha-icon-button"; import "../../components/ha-items-display-editor"; import type { DisplayValue } from "../../components/ha-items-display-editor"; import "../../components/ha-md-dialog"; import type { HaMdDialog } from "../../components/ha-md-dialog"; import { computePanels, PANEL_ICONS } from "../../components/ha-sidebar"; +import "../../components/ha-spinner"; +import { + fetchFrontendUserData, + saveFrontendUserData, +} from "../../data/frontend"; import type { HomeAssistant } from "../../types"; -import type { EditSidebarDialogParams } from "./show-dialog-edit-sidebar"; +import { showConfirmationDialog } from "../generic/show-dialog-box"; @customElement("dialog-edit-sidebar") class DialogEditSidebar extends LitElement { @@ -22,21 +29,43 @@ class DialogEditSidebar extends LitElement { @query("ha-md-dialog") private _dialog?: HaMdDialog; - @state() private _order: string[] = []; + @state() private _order?: string[]; - @state() private _hidden: string[] = []; + @state() private _hidden?: string[]; - private _saveCallback?: (order: string[], hidden: string[]) => void; + @state() private _error?: string; - public async showDialog(params: EditSidebarDialogParams): Promise { + /** + * If user has old localStorage values, show a confirmation dialog + */ + @state() private _migrateToUserData = false; + + public async showDialog(): Promise { this._open = true; - const storedOrder = localStorage.getItem("sidebarPanelOrder"); - const storedHidden = localStorage.getItem("sidebarHiddenPanels"); + this._getData(); + } - this._order = storedOrder ? JSON.parse(storedOrder) : this._order; - this._hidden = storedHidden ? JSON.parse(storedHidden) : this._hidden; - this._saveCallback = params.saveCallback; + private async _getData() { + try { + const data = await fetchFrontendUserData(this.hass.connection, "sidebar"); + this._order = data?.panelOrder; + this._hidden = data?.hiddenPanels; + + // fallback to old localStorage values + if (!this._order) { + const storedOrder = localStorage.getItem("sidebarPanelOrder"); + this._migrateToUserData = !!storedOrder; + this._order = storedOrder ? JSON.parse(storedOrder) : []; + } + if (!this._hidden) { + const storedHidden = localStorage.getItem("sidebarHiddenPanels"); + this._migrateToUserData = this._migrateToUserData || !!storedHidden; + this._hidden = storedHidden ? JSON.parse(storedHidden) : []; + } + } catch (err: any) { + this._error = err.message || err; + } } private _dialogClosed(): void { @@ -52,12 +81,16 @@ class DialogEditSidebar extends LitElement { panels ? Object.values(panels) : [] ); - protected render() { - if (!this._open) { - return nothing; + private _renderContent(): TemplateResult { + if (!this._order || !this._hidden) { + return html``; } - const dialogTitle = this.hass.localize("ui.sidebar.edit_sidebar"); + if (this._error) { + return html`${this._error}`; + } const panels = this._panels(this.hass.panels); @@ -71,7 +104,7 @@ class DialogEditSidebar extends LitElement { const items = [ ...beforeSpacer, - ...panels.filter((panel) => this._hidden.includes(panel.url_path)), + ...panels.filter((panel) => this._hidden!.includes(panel.url_path)), ...afterSpacer.filter((panel) => panel.url_path !== "config"), ].map((panel) => ({ value: panel.url_path, @@ -89,6 +122,26 @@ class DialogEditSidebar extends LitElement { disableSorting: panel.url_path === "developer-tools", })); + return html` + `; + } + + protected render() { + if (!this._open) { + return nothing; + } + + const dialogTitle = this.hass.localize("ui.sidebar.edit_sidebar"); + return html` @@ -98,26 +151,22 @@ class DialogEditSidebar extends LitElement { .path=${mdiClose} @click=${this.closeDialog} > - ${dialogTitle} + ${dialogTitle} + ${!this._migrateToUserData + ? html`${this.hass.localize("ui.sidebar.edit_subtitle")}` + : nothing} -
- - -
+
${this._renderContent()}
${this.hass.localize("ui.common.cancel")} - + ${this.hass.localize("ui.common.save")}
@@ -131,8 +180,27 @@ class DialogEditSidebar extends LitElement { this._hidden = [...hidden]; } - private _save(): void { - this._saveCallback?.(this._order ?? [], this._hidden ?? []); + private async _save() { + if (this._migrateToUserData) { + const confirmation = await showConfirmationDialog(this, { + destructive: true, + text: this.hass.localize("ui.sidebar.migrate_to_user_data"), + }); + if (!confirmation) { + return; + } + } + + try { + await saveFrontendUserData(this.hass.connection, "sidebar", { + panelOrder: this._order!, + hiddenPanels: this._hidden!, + }); + } catch (err: any) { + this._error = err.message || err; + return; + } + this.closeDialog(); } @@ -149,6 +217,12 @@ class DialogEditSidebar extends LitElement { min-height: 100%; } } + + ha-fade-in { + display: flex; + justify-content: center; + align-items: center; + } `; } diff --git a/src/dialogs/sidebar/show-dialog-edit-sidebar.ts b/src/dialogs/sidebar/show-dialog-edit-sidebar.ts index 4a88bafd6b..dc8ce5b1ea 100644 --- a/src/dialogs/sidebar/show-dialog-edit-sidebar.ts +++ b/src/dialogs/sidebar/show-dialog-edit-sidebar.ts @@ -1,18 +1,11 @@ import { fireEvent } from "../../common/dom/fire_event"; -export interface EditSidebarDialogParams { - saveCallback: (order: string[], hidden: string[]) => void; -} - export const loadEditSidebarDialog = () => import("./dialog-edit-sidebar"); -export const showEditSidebarDialog = ( - element: HTMLElement, - dialogParams: EditSidebarDialogParams -): void => { +export const showEditSidebarDialog = (element: HTMLElement): void => { fireEvent(element, "show-dialog", { dialogTag: "dialog-edit-sidebar", dialogImport: loadEditSidebarDialog, - dialogParams, + dialogParams: {}, }); }; diff --git a/src/layouts/home-assistant-main.ts b/src/layouts/home-assistant-main.ts index 75d5a54765..16656921c8 100644 --- a/src/layouts/home-assistant-main.ts +++ b/src/layouts/home-assistant-main.ts @@ -5,31 +5,23 @@ import type { HASSDomEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event"; import { listenMediaQuery } from "../common/dom/media_query"; import { toggleAttribute } from "../common/dom/toggle_attribute"; +import { computeRTLDirection } from "../common/util/compute_rtl"; import "../components/ha-drawer"; import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer"; import type { HomeAssistant, Route } from "../types"; import "./partial-panel-resolver"; -import { computeRTLDirection } from "../common/util/compute_rtl"; -import { storage } from "../common/decorators/storage"; declare global { // for fire event interface HASSDomEvents { "hass-toggle-menu": undefined | { open?: boolean }; - "hass-edit-sidebar": EditSideBarEvent; "hass-show-notifications": undefined; } interface HTMLElementEventMap { - "hass-edit-sidebar": HASSDomEvent; "hass-toggle-menu": HASSDomEvent; } } -interface EditSideBarEvent { - order: string[]; - hidden: string[]; -} - @customElement("home-assistant-main") export class HomeAssistantMain extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -44,22 +36,6 @@ export class HomeAssistantMain extends LitElement { @state() private _drawerOpen = false; - @state() - @storage({ - key: "sidebarPanelOrder", - state: true, - subscribe: true, - }) - private _panelOrder: string[] = []; - - @state() - @storage({ - key: "sidebarHiddenPanels", - state: true, - subscribe: true, - }) - private _hiddenPanels: string[] = []; - constructor() { super(); listenMediaQuery("(max-width: 870px)", (matches) => { @@ -81,8 +57,6 @@ export class HomeAssistantMain extends LitElement { .hass=${this.hass} .narrow=${sidebarNarrow} .route=${this.route} - .panelOrder=${this._panelOrder} - .hiddenPanels=${this._hiddenPanels} .alwaysExpand=${sidebarNarrow || this.hass.dockedSidebar === "docked"} > ) => { - this._panelOrder = ev.detail.order; - this._hiddenPanels = ev.detail.hidden; - } - ); - this.addEventListener("hass-toggle-menu", (ev) => { if (this._sidebarEditMode) { return; diff --git a/src/panels/profile/ha-profile-section-general.ts b/src/panels/profile/ha-profile-section-general.ts index a14043f625..46cbc1c268 100644 --- a/src/panels/profile/ha-profile-section-general.ts +++ b/src/panels/profile/ha-profile-section-general.ts @@ -150,6 +150,23 @@ class HaProfileSectionGeneral extends LitElement { .narrow=${this.narrow} .hass=${this.hass} > + + + ${this.hass.localize( + "ui.panel.profile.customize_sidebar.header" + )} + + + ${this.hass.localize( + "ui.panel.profile.customize_sidebar.description" + )} + + + ${this.hass.localize( + "ui.panel.profile.customize_sidebar.button" + )} + + ${this.hass.user!.is_admin ? html` - - - ${this.hass.localize( - "ui.panel.profile.customize_sidebar.header" - )} - - - ${this.hass.localize( - "ui.panel.profile.customize_sidebar.description" - )} - - - ${this.hass.localize( - "ui.panel.profile.customize_sidebar.button" - )} - - ${this.hass.dockedSidebar !== "auto" || !this.narrow ? html` { - fireEvent(this, "hass-edit-sidebar", { - order, - hidden, - }); - }; - private _handleLogOut() { showConfirmationDialog(this, { title: this.hass.localize("ui.panel.profile.logout_title"), diff --git a/src/translations/en.json b/src/translations/en.json index 725074c0ca..ff6bd50f71 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2019,7 +2019,9 @@ "sidebar": { "external_app_configuration": "App settings", "sidebar_toggle": "Sidebar toggle", - "edit_sidebar": "Edit sidebar" + "edit_sidebar": "Edit sidebar", + "edit_subtitle": "Synced on all devices", + "migrate_to_user_data": "This will change the sidebar on all the devices you are logged into. To create a sidebar per device, you should use a different user for that device." }, "panel": { "my": { From bb5f01ac81d7e57ce76829609b928a1805fb56c4 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Mon, 26 May 2025 15:25:57 +0200 Subject: [PATCH 28/30] Use failed add-ons and folders of backup (#25548) Co-authored-by: Bram Kragten --- src/data/backup.ts | 8 + .../components/ha-backup-details-summary.ts | 70 +++- .../overview/ha-backup-overview-summary.ts | 385 ++++++++---------- src/translations/en.json | 14 +- 4 files changed, 261 insertions(+), 216 deletions(-) diff --git a/src/data/backup.ts b/src/data/backup.ts index d37c50f431..12261eb810 100644 --- a/src/data/backup.ts +++ b/src/data/backup.ts @@ -103,12 +103,20 @@ export interface BackupContentAgent { protected: boolean; } +export interface AddonInfo { + name: string | null; + slug: string; + version: string | null; +} + export interface BackupContent { backup_id: string; date: string; name: string; agents: Record; failed_agent_ids?: string[]; + failed_addons?: AddonInfo[]; + failed_folders?: string[]; extra_metadata?: { "supervisor.addon_update"?: string; }; diff --git a/src/panels/config/backup/components/ha-backup-details-summary.ts b/src/panels/config/backup/components/ha-backup-details-summary.ts index d855696bbe..89f69bc5df 100644 --- a/src/panels/config/backup/components/ha-backup-details-summary.ts +++ b/src/panels/config/backup/components/ha-backup-details-summary.ts @@ -1,15 +1,17 @@ -import { css, html, LitElement } from "lit"; +import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; +import { formatDateTime } from "../../../../common/datetime/format_date_time"; +import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter"; +import "../../../../components/ha-alert"; import "../../../../components/ha-card"; import "../../../../components/ha-md-list"; import "../../../../components/ha-md-list-item"; -import type { HomeAssistant } from "../../../../types"; -import { formatDateTime } from "../../../../common/datetime/format_date_time"; import { computeBackupSize, computeBackupType, type BackupContentExtended, } from "../../../../data/backup"; +import type { HomeAssistant } from "../../../../types"; import { bytesToString } from "../../../../util/bytes-to-string"; @customElement("ha-backup-details-summary") @@ -28,12 +30,35 @@ class HaBackupDetailsSummary extends LitElement { this.hass.config ); + const errors: { title: string; items: string[] }[] = []; + if (this.backup.failed_addons?.length) { + errors.push({ + title: this.hass.localize( + "ui.panel.config.backup.details.summary.error.failed_addons" + ), + items: this.backup.failed_addons.map( + (addon) => `${addon.name || addon.slug} (${addon.version})` + ), + }); + } + if (this.backup.failed_folders?.length) { + errors.push({ + title: this.hass.localize( + "ui.panel.config.backup.details.summary.error.failed_folders" + ), + items: this.backup.failed_folders.map((folder) => + this._localizeFolder(folder) + ), + }); + } + return html`
${this.hass.localize("ui.panel.config.backup.details.summary.title")}
+ ${errors.length ? this._renderErrorSummary(errors) : nothing} @@ -69,6 +94,45 @@ class HaBackupDetailsSummary extends LitElement { `; } + private _renderErrorSummary(errors: { title: string; items: string[] }[]) { + return html` + + ${errors.map( + ({ title, items }) => html` +
+ ${title}: +
    + ${items.map((item) => html`
  • ${item}
  • `)} +
+ ` + )} +
+ `; + } + + private _localizeFolder(folder: string): string { + switch (folder) { + case "media": + return this.hass.localize(`ui.panel.config.backup.data_picker.media`); + case "share": + return this.hass.localize( + `ui.panel.config.backup.data_picker.share_folder` + ); + case "ssl": + return this.hass.localize(`ui.panel.config.backup.data_picker.ssl`); + case "addons/local": + return this.hass.localize( + `ui.panel.config.backup.data_picker.local_addons` + ); + } + return capitalizeFirstLetter(folder); + } + static styles = css` :host { max-width: 690px; diff --git a/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts b/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts index b238ad9767..7488440e5d 100644 --- a/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts +++ b/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts @@ -4,13 +4,18 @@ import type { CSSResultGroup } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import memoizeOne from "memoize-one"; +import { + formatDate, + formatDateWeekday, +} from "../../../../../common/datetime/format_date"; import { relativeTime } from "../../../../../common/datetime/relative_time"; +import type { LocalizeKeys } from "../../../../../common/translations/localize"; import "../../../../../components/ha-button"; import "../../../../../components/ha-card"; +import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-md-list"; import "../../../../../components/ha-md-list-item"; import "../../../../../components/ha-svg-icon"; -import "../../../../../components/ha-icon-button"; import type { BackupConfig, BackupContent } from "../../../../../data/backup"; import { BackupScheduleRecurrence, @@ -18,12 +23,8 @@ import { } from "../../../../../data/backup"; import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant } from "../../../../../types"; -import "../ha-backup-summary-card"; -import { - formatDate, - formatDateWeekday, -} from "../../../../../common/datetime/format_date"; import { showAlertDialog } from "../../../../lovelace/custom-card-helpers"; +import "../ha-backup-summary-card"; const OVERDUE_MARGIN_HOURS = 3; @@ -55,29 +56,57 @@ class HaBackupOverviewBackups extends LitElement { ); }); + private _renderSummaryCard( + heading: string, + status: "error" | "info" | "warning" | "loading" | "success", + headline: string | null, + description?: string | null, + lastCompletedDate?: Date + ) { + return html` + + + + + ${headline} + + ${description || description === null + ? html` + + ${description} + + ${lastCompletedDate + ? html` ` + : nothing} + ` + : nothing} + + + `; + } + protected render() { const now = new Date(); if (this.fetching) { - return html` - - - - - - - - - - - - - `; + return this._renderSummaryCard( + this.hass.localize("ui.panel.config.backup.overview.summary.loading"), + "loading", + null, + null + ); } const lastBackup = this._lastBackup(this.backups); @@ -137,146 +166,112 @@ class HaBackupOverviewBackups extends LitElement { if (lastAttemptDate > lastCompletedDate) { const lastUploadedBackup = this._lastUploadedBackup(this.backups); - return html` - - - - - - ${this.hass.localize( - "ui.panel.config.backup.overview.summary.last_backup_failed_description", - { - relative_time: relativeTime( - lastAttemptDate, - this.hass.locale, - now, - true - ), - } - )} - - - ${lastUploadedBackup || nextBackupDescription - ? html` - - - - ${lastUploadedBackup - ? this.hass.localize( - "ui.panel.config.backup.overview.summary.last_successful_backup_description", - { - relative_time: relativeTime( - new Date(lastUploadedBackup.date), - this.hass.locale, - now, - true - ), - count: Object.keys(lastUploadedBackup.agents) - .length, - } - ) - : nextBackupDescription} - - - ` - : nothing} - - - `; + return this._renderSummaryCard( + this.hass.localize( + "ui.panel.config.backup.overview.summary.last_backup_failed_heading" + ), + "error", + this.hass.localize( + "ui.panel.config.backup.overview.summary.last_backup_failed_description", + { + relative_time: relativeTime( + lastAttemptDate, + this.hass.locale, + now, + true + ), + } + ), + lastUploadedBackup || nextBackupDescription + ? lastUploadedBackup + ? this.hass.localize( + "ui.panel.config.backup.overview.summary.last_successful_backup_description", + { + relative_time: relativeTime( + new Date(lastUploadedBackup.date), + this.hass.locale, + now, + true + ), + count: Object.keys(lastUploadedBackup.agents).length, + } + ) + : nextBackupDescription + : undefined + ); } // If no backups yet, show warning if (!lastBackup) { - return html` - - - - - - ${this.hass.localize( - "ui.panel.config.backup.overview.summary.no_backup_description" - )} - - - ${this._renderNextBackupDescription( - nextBackupDescription, - lastCompletedDate, - showAdditionalBackupDescription - )} - - - `; + return this._renderSummaryCard( + this.hass.localize( + "ui.panel.config.backup.overview.summary.no_backup_heading" + ), + "warning", + this.hass.localize( + "ui.panel.config.backup.overview.summary.no_backup_description" + ), + nextBackupDescription, + showAdditionalBackupDescription ? lastCompletedDate : undefined + ); } const lastBackupDate = new Date(lastBackup.date); - // If last backup - if (lastBackup.failed_agent_ids?.length) { + // if parts of the last backup failed + if ( + lastBackup.failed_agent_ids?.length || + lastBackup.failed_addons?.length || + lastBackup.failed_folders?.length + ) { const lastUploadedBackup = this._lastUploadedBackup(this.backups); - return html` - - - - - - ${this.hass.localize( - "ui.panel.config.backup.overview.summary.last_backup_failed_locations_description", - { - relative_time: relativeTime( - lastAttemptDate, - this.hass.locale, - now, - true - ), - } - )} - - + const failedTypes: string[] = []; - ${lastUploadedBackup || nextBackupDescription - ? html` - - - ${lastUploadedBackup - ? this.hass.localize( - "ui.panel.config.backup.overview.summary.last_successful_backup_description", - { - relative_time: relativeTime( - new Date(lastUploadedBackup.date), - this.hass.locale, - now, - true - ), - count: Object.keys(lastUploadedBackup.agents) - .length, - } - ) - : nextBackupDescription} - - ` - : nothing} - - - `; + if (lastBackup.failed_agent_ids?.length) { + failedTypes.push("locations"); + } + if (lastBackup.failed_addons?.length) { + failedTypes.push("addons"); + } + if (lastBackup.failed_folders?.length) { + failedTypes.push("folders"); + } + + const type = failedTypes.join("_"); + + return this._renderSummaryCard( + this.hass.localize( + "ui.panel.config.backup.overview.summary.last_backup_failed_heading" + ), + "error", + this.hass.localize( + `ui.panel.config.backup.overview.summary.last_backup_failed_${type}_description` as LocalizeKeys, + { + relative_time: relativeTime( + lastAttemptDate, + this.hass.locale, + now, + true + ), + } + ), + lastUploadedBackup + ? this.hass.localize( + "ui.panel.config.backup.overview.summary.last_successful_backup_description", + { + relative_time: relativeTime( + new Date(lastUploadedBackup.date), + this.hass.locale, + now, + true + ), + count: Object.keys(lastUploadedBackup.agents).length, + } + ) + : nextBackupDescription, + showAdditionalBackupDescription ? lastCompletedDate : undefined + ); } const lastSuccessfulBackupDescription = this.hass.localize( @@ -303,67 +298,33 @@ class HaBackupOverviewBackups extends LitElement { this.config.schedule.recurrence === BackupScheduleRecurrence.DAILY) || numberOfDays >= 7; - return html` - - - - - ${lastSuccessfulBackupDescription} - - ${this._renderNextBackupDescription( - nextBackupDescription, - lastCompletedDate, - showAdditionalBackupDescription - )} - - - `; + return this._renderSummaryCard( + this.hass.localize( + `ui.panel.config.backup.overview.summary.${isOverdue ? "backup_too_old_heading" : "backup_success_heading"}`, + { count: numberOfDays } + ), + isOverdue ? "warning" : "success", + lastSuccessfulBackupDescription, + nextBackupDescription, + showAdditionalBackupDescription ? lastCompletedDate : undefined + ); } - private _renderNextBackupDescription( - nextBackupDescription: string, - lastCompletedDate: Date, - showTip = false - ) { - // handle edge case that there is an additional backup scheduled - const openAdditionalBackupDescriptionDialog = showTip - ? () => { - showAlertDialog(this, { - text: this.hass.localize( - "ui.panel.config.backup.overview.summary.additional_backup_description", - { - date: formatDate( - lastCompletedDate, - this.hass.locale, - this.hass.config - ), - } + private _createAdditionalBackupDescription = + (lastCompletedDate: Date) => () => { + showAlertDialog(this, { + text: this.hass.localize( + "ui.panel.config.backup.overview.summary.additional_backup_description", + { + date: formatDate( + lastCompletedDate, + this.hass.locale, + this.hass.config ), - }); - } - : undefined; - - return nextBackupDescription - ? html` - - ${nextBackupDescription} - - ${showTip - ? html` ` - : nothing} - ` - : nothing; - } + } + ), + }); + }; static get styles(): CSSResultGroup { return [ diff --git a/src/translations/en.json b/src/translations/en.json index ff6bd50f71..43ca921f43 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2654,6 +2654,12 @@ "last_backup_failed_heading": "Last automatic backup failed", "last_backup_failed_description": "The last automatic backup triggered {relative_time} wasn't successful.", "last_backup_failed_locations_description": "The last automatic backup created {relative_time} wasn't stored in all locations.", + "last_backup_failed_addons_description": "The last automatic backup created {relative_time} was not able to backup all add-ons.", + "last_backup_failed_folders_description": "The last automatic backup created {relative_time} was not able to backup all folders.", + "last_backup_failed_addons_folders_description": "The last automatic backup created {relative_time} wasn't able to backup all add-ons and folders.", + "last_backup_failed_locations_addons_description": "The last automatic backup created {relative_time} wasn't able to backup all add-ons and wasn't stored in all locations.", + "last_backup_failed_locations_folders_description": "The last automatic backup created {relative_time} wasn't able to backup all folders and wasn't stored in all locations.", + "last_backup_failed_locations_addons_folders_description": "The last automatic backup created {relative_time} wasn't able to backup all add-ons and folders and wasn't stored in all locations.", "last_successful_backup_description": "Last successful automatic backup {relative_time} and stored in {count} {count, plural,\n one {location}\n other {locations}\n}.", "no_backup_heading": "No automatic backup available", "no_backup_description": "You have no automatic backups yet.", @@ -2769,7 +2775,13 @@ "summary": { "title": "Backup", "size": "Size", - "created": "Created" + "created": "Created", + "error": { + "title": "This backup was not created successfully. Some data is missing.", + "failed_locations": "Failed locations", + "failed_addons": "Failed add-ons", + "failed_folders": "Failed folders" + } }, "restore": { "title": "Select what to restore", From 3ce639946c06e1de28a1309ac396b88e5fde4e7d Mon Sep 17 00:00:00 2001 From: Cretezy Date: Mon, 26 May 2025 09:26:32 -0400 Subject: [PATCH 29/30] Make weather-forecast card more responsive (#24900) Co-authored-by: Bram Kragten --- .../cards/hui-weather-forecast-card.ts | 90 ++++++++++++++++--- 1 file changed, 76 insertions(+), 14 deletions(-) diff --git a/src/panels/lovelace/cards/hui-weather-forecast-card.ts b/src/panels/lovelace/cards/hui-weather-forecast-card.ts index 4b9d49c809..2140038978 100644 --- a/src/panels/lovelace/cards/hui-weather-forecast-card.ts +++ b/src/panels/lovelace/cards/hui-weather-forecast-card.ts @@ -3,6 +3,7 @@ import type { CSSResultGroup, PropertyValues } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; +import { classMap } from "lit/directives/class-map"; import { formatDateWeekdayShort } from "../../../common/datetime/format_date"; import { formatTime } from "../../../common/datetime/format_time"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; @@ -74,17 +75,26 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { private _sizeController = new ResizeController(this, { callback: (entries) => { + const result = { + width: "regular", + height: "tall", + }; + const width = entries[0]?.contentRect.width; if (width < 245) { - return "very-very-narrow"; + result.height = "very-very-narrow"; + } else if (width < 300) { + result.width = "very-narrow"; + } else if (width < 375) { + result.width = "narrow"; } - if (width < 300) { - return "very-narrow"; + + const height = entries[0]?.contentRect.height; + if (height < 235) { + result.height = "short"; } - if (width < 375) { - return "narrow"; - } - return "regular"; + + return result; }, }); @@ -233,11 +243,11 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { ); let itemsToShow = this._config?.forecast_slots ?? 5; - if (this._sizeController.value === "very-very-narrow") { + if (this._sizeController.value.width === "very-very-narrow") { itemsToShow = Math.min(3, itemsToShow); - } else if (this._sizeController.value === "very-narrow") { + } else if (this._sizeController.value.width === "very-narrow") { itemsToShow = Math.min(5, itemsToShow); - } else if (this._sizeController.value === "narrow") { + } else if (this._sizeController.value.width === "narrow") { itemsToShow = Math.min(7, itemsToShow); } @@ -255,7 +265,10 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { return html` * { + flex: 0 0 48px; + height: 48px; + } + + [class*="very-very-narrow"] .content + .forecast { + padding-top: 8px; + } + [class*="very-very-narrow"] .icon-image { margin-right: 0; margin-inline-end: 0; margin-inline-start: initial; } + + /* ============= SHORT ============= */ + + .short .state, + .short .temp-attribute .temp { + font-size: 24px; + line-height: 1.25; + } + + .short .content + .forecast { + padding-top: 12px; + } + + .short .icon-image { + min-width: 48px; + } + + .short .icon-image > * { + flex: 0 0 48px; + height: 48px; + } + + .short .forecast-image-icon { + padding-top: 4px; + padding-bottom: 4px; + } + + .short .forecast-image-icon > * { + width: 32px; + height: 32px; + --mdc-icon-size: 32px; + } + + .short .forecast-icon { + --mdc-icon-size: 32px; + } `, ]; } From fcf5ed7731732920cff3831921efd5a655f8ff18 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 26 May 2025 15:59:26 +0200 Subject: [PATCH 30/30] Use context instead of stateObj for card features (#25577) Co-authored-by: Bram Kragten --- src/data/lovelace_custom_cards.ts | 7 ++ .../hui-alarm-modes-card-feature.ts | 64 +++++++--- .../card-features/hui-card-feature.ts | 25 ++-- .../card-features/hui-card-features.ts | 10 +- .../hui-climate-fan-modes-card-feature.ts | 54 ++++++--- .../hui-climate-hvac-modes-card-feature.ts | 58 ++++++--- .../hui-climate-preset-modes-card-feature.ts | 54 ++++++--- ...ate-swing-horizontal-modes-card-feature.ts | 54 ++++++--- .../hui-climate-swing-modes-card-feature.ts | 54 ++++++--- .../hui-counter-actions-card-feature.ts | 41 +++++-- .../hui-cover-open-close-card-feature.ts | 54 ++++++--- .../hui-cover-position-card-feature.ts | 47 +++++--- .../hui-cover-tilt-card-feature.ts | 50 +++++--- .../hui-cover-tilt-position-card-feature.ts | 41 +++++-- .../hui-fan-preset-modes-card-feature.ts | 53 ++++++--- .../hui-fan-speed-card-feature.ts | 59 ++++++---- .../hui-humidifier-modes-card-feature.ts | 54 ++++++--- .../hui-humidifier-toggle-card-feature.ts | 54 ++++++--- .../hui-lawn-mower-commands-card-feature.ts | 39 ++++-- .../hui-light-brightness-card-feature.ts | 41 +++++-- .../hui-light-color-temp-card-feature.ts | 49 +++++--- .../hui-lock-commands-card-feature.ts | 39 ++++-- .../hui-lock-open-door-card-feature.ts | 37 ++++-- ...media-player-volume-slider-card-feature.ts | 46 ++++++-- .../hui-numeric-input-card-feature.ts | 43 +++++-- .../hui-select-options-card-feature.ts | 59 +++++++--- .../hui-target-humidity-card-feature.ts | 57 ++++++--- .../hui-target-temperature-card-feature.ts | 111 +++++++++++------- .../card-features/hui-toggle-card-feature.ts | 52 +++++--- .../hui-update-actions-card-feature.ts | 41 +++++-- .../hui-vacuum-commands-card-feature.ts | 44 +++++-- ...ter-heater-operation-modes-card-feature.ts | 54 ++++++--- .../lovelace/cards/hui-humidifier-card.ts | 8 +- .../lovelace/cards/hui-thermostat-card.ts | 8 +- src/panels/lovelace/cards/hui-tile-card.ts | 8 +- .../hui-card-features-editor.ts | 70 ++++++++--- .../hui-humidifier-card-editor.ts | 13 +- .../hui-thermostat-card-editor.ts | 13 +- .../config-elements/hui-tile-card-editor.ts | 22 ++-- .../areas/helpers/areas-strategy-helper.ts | 19 ++- src/panels/lovelace/types.ts | 13 +- 41 files changed, 1229 insertions(+), 490 deletions(-) diff --git a/src/data/lovelace_custom_cards.ts b/src/data/lovelace_custom_cards.ts index c79cfb4584..3b3a4e5486 100644 --- a/src/data/lovelace_custom_cards.ts +++ b/src/data/lovelace_custom_cards.ts @@ -1,4 +1,6 @@ import type { HassEntity } from "home-assistant-js-websocket"; +import type { HomeAssistant } from "../types"; +import type { LovelaceCardFeatureContext } from "../panels/lovelace/card-features/types"; export interface CustomCardEntry { type: string; @@ -19,7 +21,12 @@ export interface CustomBadgeEntry { export interface CustomCardFeatureEntry { type: string; name?: string; + /** @deprecated Use `isSupported` */ supported?: (stateObj: HassEntity) => boolean; + isSupported?: ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext + ) => boolean; configurable?: boolean; } diff --git a/src/panels/lovelace/card-features/hui-alarm-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-alarm-modes-card-feature.ts index 6aa71fe590..13524cac99 100644 --- a/src/panels/lovelace/card-features/hui-alarm-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-alarm-modes-card-feature.ts @@ -1,7 +1,6 @@ import { mdiShieldOff } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; -import { html, LitElement } from "lit"; +import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; @@ -26,9 +25,19 @@ import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; import { filterModes } from "./common/filter-modes"; -import type { AlarmModesCardFeatureConfig } from "./types"; +import type { + AlarmModesCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsAlarmModesCardFeature = (stateObj: HassEntity) => { +export const supportsAlarmModesCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return domain === "alarm_control_panel"; }; @@ -40,7 +49,7 @@ class HuiAlarmModeCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: AlarmControlPanelEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: AlarmModesCardFeatureConfig; @@ -66,10 +75,26 @@ class HuiAlarmModeCardFeature this._config = config; } + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id] as + | AlarmControlPanelEntity + | undefined; + } + protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentMode = this._getCurrentMode(this.stateObj); + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentMode = this._getCurrentMode(this._stateObj); + } } } @@ -79,12 +104,12 @@ class HuiAlarmModeCardFeature }); private async _valueChanged(ev: CustomEvent) { - if (!this.stateObj) return; + if (!this._stateObj) return; const mode = (ev.detail as any).value as AlarmMode; - if (mode === this.stateObj.state) return; + if (mode === this._stateObj.state) return; - const oldMode = this._getCurrentMode(this.stateObj); + const oldMode = this._getCurrentMode(this._stateObj); this._currentMode = mode; try { @@ -102,24 +127,25 @@ class HuiAlarmModeCardFeature await setProtectedAlarmControlPanelMode( this, this.hass!, - this.stateObj!, + this._stateObj!, mode ); } - protected render(): TemplateResult | null { + protected render(): TemplateResult | typeof nothing { if ( !this._config || !this.hass || - !this.stateObj || - !supportsAlarmModesCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsAlarmModesCardFeature(this.hass, this.context) ) { - return null; + return nothing; } - const color = stateColorCss(this.stateObj); + const color = stateColorCss(this._stateObj); - const supportedModes = supportedAlarmModes(this.stateObj).reverse(); + const supportedModes = supportedAlarmModes(this._stateObj).reverse(); const options = filterModes( supportedModes, @@ -130,7 +156,7 @@ class HuiAlarmModeCardFeature path: ALARM_MODES[mode].path, })); - if (["triggered", "arming", "pending"].includes(this.stateObj.state)) { + if (["triggered", "arming", "pending"].includes(this._stateObj.state)) { return html` `; diff --git a/src/panels/lovelace/card-features/hui-card-feature.ts b/src/panels/lovelace/card-features/hui-card-feature.ts index 7a6d3668e7..12592b4fba 100644 --- a/src/panels/lovelace/card-features/hui-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-card-feature.ts @@ -1,17 +1,19 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import { LitElement, html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import type { HomeAssistant } from "../../../types"; import type { HuiErrorCard } from "../cards/hui-error-card"; import { createCardFeatureElement } from "../create-element/create-card-feature-element"; import type { LovelaceCardFeature } from "../types"; -import type { LovelaceCardFeatureConfig } from "./types"; +import type { + LovelaceCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; @customElement("hui-card-feature") export class HuiCardFeature extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public stateObj!: HassEntity; + @property({ attribute: false }) public context!: LovelaceCardFeatureContext; @property({ attribute: false }) public feature?: LovelaceCardFeatureConfig; @@ -22,9 +24,7 @@ export class HuiCardFeature extends LitElement { private _getFeatureElement(feature: LovelaceCardFeatureConfig) { if (!this._element) { this._element = createCardFeatureElement(feature); - return this._element; } - return this._element; } @@ -33,12 +33,21 @@ export class HuiCardFeature extends LitElement { return nothing; } - const element = this._getFeatureElement(this.feature); + const element = this._getFeatureElement( + this.feature + ) as LovelaceCardFeature; if (this.hass) { element.hass = this.hass; - (element as LovelaceCardFeature).stateObj = this.stateObj; - (element as LovelaceCardFeature).color = this.color; + element.context = this.context; + element.color = this.color; + // Backwards compatibility from custom card features + if (this.context.entity_id) { + const stateObj = this.hass.states[this.context.entity_id]; + if (stateObj) { + element.stateObj = stateObj; + } + } } return html`${element}`; } diff --git a/src/panels/lovelace/card-features/hui-card-features.ts b/src/panels/lovelace/card-features/hui-card-features.ts index 7253bb7895..b723c3f2a0 100644 --- a/src/panels/lovelace/card-features/hui-card-features.ts +++ b/src/panels/lovelace/card-features/hui-card-features.ts @@ -1,15 +1,17 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import type { HomeAssistant } from "../../../types"; import "./hui-card-feature"; -import type { LovelaceCardFeatureConfig } from "./types"; +import type { + LovelaceCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; @customElement("hui-card-features") export class HuiCardFeatures extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public stateObj!: HassEntity; + @property({ attribute: false }) public context!: LovelaceCardFeatureContext; @property({ attribute: false }) public features?: LovelaceCardFeatureConfig[]; @@ -24,7 +26,7 @@ export class HuiCardFeatures extends LitElement { (feature) => html` diff --git a/src/panels/lovelace/card-features/hui-climate-fan-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-fan-modes-card-feature.ts index 45425225d6..7ba6cab9e0 100644 --- a/src/panels/lovelace/card-features/hui-climate-fan-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-fan-modes-card-feature.ts @@ -1,5 +1,4 @@ import { mdiFan } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -19,9 +18,19 @@ import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; import { filterModes } from "./common/filter-modes"; -import type { ClimateFanModesCardFeatureConfig } from "./types"; +import type { + ClimateFanModesCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsClimateFanModesCardFeature = (stateObj: HassEntity) => { +export const supportsClimateFanModesCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return ( domain === "climate" && @@ -36,7 +45,7 @@ class HuiClimateFanModesCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: ClimateEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: ClimateFanModesCardFeatureConfig; @@ -45,6 +54,15 @@ class HuiClimateFanModesCardFeature @query("ha-control-select-menu", true) private _haSelect?: HaControlSelectMenu; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | ClimateEntity + | undefined; + } + static getStubConfig(): ClimateFanModesCardFeatureConfig { return { type: "climate-fan-modes", @@ -68,8 +86,15 @@ class HuiClimateFanModesCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentFanMode = this.stateObj.attributes.fan_mode; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentFanMode = this._stateObj.attributes.fan_mode; + } } } @@ -91,7 +116,7 @@ class HuiClimateFanModesCardFeature const fanMode = (ev.detail as any).value ?? ((ev.target as any).value as string); - const oldFanMode = this.stateObj!.attributes.fan_mode; + const oldFanMode = this._stateObj!.attributes.fan_mode; if (fanMode === oldFanMode) return; @@ -106,7 +131,7 @@ class HuiClimateFanModesCardFeature private async _setMode(mode: string) { await this.hass!.callService("climate", "set_fan_mode", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, fan_mode: mode, }); } @@ -115,13 +140,14 @@ class HuiClimateFanModesCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsClimateFanModesCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsClimateFanModesCardFeature(this.hass, this.context) ) { return null; } - const stateObj = this.stateObj; + const stateObj = this._stateObj; const options = filterModes( stateObj.attributes.fan_modes, @@ -129,7 +155,7 @@ class HuiClimateFanModesCardFeature ).map((mode) => ({ value: mode, label: this.hass!.formatEntityAttributeValue( - this.stateObj!, + this._stateObj!, "fan_mode", mode ), @@ -153,7 +179,7 @@ class HuiClimateFanModesCardFeature stateObj, "fan_mode" )} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} > `; @@ -165,7 +191,7 @@ class HuiClimateFanModesCardFeature hide-label .label=${this.hass!.formatEntityAttributeName(stateObj, "fan_mode")} .value=${this._currentFanMode} - .disabled=${this.stateObj.state === UNAVAILABLE} + .disabled=${this._stateObj.state === UNAVAILABLE} fixedMenuPosition naturalMenuWidth @selected=${this._valueChanged} diff --git a/src/panels/lovelace/card-features/hui-climate-hvac-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-hvac-modes-card-feature.ts index 266e2820c1..16168289f7 100644 --- a/src/panels/lovelace/card-features/hui-climate-hvac-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-hvac-modes-card-feature.ts @@ -1,5 +1,4 @@ import { mdiThermostat } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -22,9 +21,19 @@ import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; import { filterModes } from "./common/filter-modes"; -import type { ClimateHvacModesCardFeatureConfig } from "./types"; +import type { + ClimateHvacModesCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsClimateHvacModesCardFeature = (stateObj: HassEntity) => { +export const supportsClimateHvacModesCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return domain === "climate"; }; @@ -36,7 +45,7 @@ class HuiClimateHvacModesCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: ClimateEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: ClimateHvacModesCardFeatureConfig; @@ -45,6 +54,15 @@ class HuiClimateHvacModesCardFeature @query("ha-control-select-menu", true) private _haSelect?: HaControlSelectMenu; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | ClimateEntity + | undefined; + } + static getStubConfig(): ClimateHvacModesCardFeatureConfig { return { type: "climate-hvac-modes", @@ -67,8 +85,15 @@ class HuiClimateHvacModesCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentHvacMode = this.stateObj.state as HvacMode; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentHvacMode = this._stateObj.state as HvacMode; + } } } @@ -90,9 +115,9 @@ class HuiClimateHvacModesCardFeature const mode = (ev.detail as any).value ?? ((ev.target as any).value as HvacMode); - if (mode === this.stateObj!.state) return; + if (mode === this._stateObj!.state) return; - const oldMode = this.stateObj!.state as HvacMode; + const oldMode = this._stateObj!.state as HvacMode; this._currentHvacMode = mode; try { @@ -104,7 +129,7 @@ class HuiClimateHvacModesCardFeature private async _setMode(mode: HvacMode) { await this.hass!.callService("climate", "set_hvac_mode", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, hvac_mode: mode, }); } @@ -113,15 +138,16 @@ class HuiClimateHvacModesCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsClimateHvacModesCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsClimateHvacModesCardFeature(this.hass, this.context) ) { return null; } - const color = stateColorCss(this.stateObj); + const color = stateColorCss(this._stateObj); - const ordererHvacModes = (this.stateObj.attributes.hvac_modes || []) + const ordererHvacModes = (this._stateObj.attributes.hvac_modes || []) .concat() .sort(compareClimateHvacModes) .reverse(); @@ -131,7 +157,7 @@ class HuiClimateHvacModesCardFeature this._config.hvac_modes ).map((mode) => ({ value: mode, - label: this.hass!.formatEntityState(this.stateObj!, mode), + label: this.hass!.formatEntityState(this._stateObj!, mode), icon: html` `; diff --git a/src/panels/lovelace/card-features/hui-climate-preset-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-preset-modes-card-feature.ts index 44086b5d14..38a49b2549 100644 --- a/src/panels/lovelace/card-features/hui-climate-preset-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-preset-modes-card-feature.ts @@ -1,5 +1,4 @@ import { mdiTuneVariant } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -19,9 +18,19 @@ import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; import { filterModes } from "./common/filter-modes"; -import type { ClimatePresetModesCardFeatureConfig } from "./types"; +import type { + ClimatePresetModesCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsClimatePresetModesCardFeature = (stateObj: HassEntity) => { +export const supportsClimatePresetModesCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return ( domain === "climate" && @@ -36,7 +45,7 @@ class HuiClimatePresetModesCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: ClimateEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: ClimatePresetModesCardFeatureConfig; @@ -45,6 +54,15 @@ class HuiClimatePresetModesCardFeature @query("ha-control-select-menu", true) private _haSelect?: HaControlSelectMenu; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | ClimateEntity + | undefined; + } + static getStubConfig(): ClimatePresetModesCardFeatureConfig { return { type: "climate-preset-modes", @@ -70,8 +88,15 @@ class HuiClimatePresetModesCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentPresetMode = this.stateObj.attributes.preset_mode; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentPresetMode = this._stateObj.attributes.preset_mode; + } } } @@ -93,7 +118,7 @@ class HuiClimatePresetModesCardFeature const presetMode = (ev.detail as any).value ?? ((ev.target as any).value as string); - const oldPresetMode = this.stateObj!.attributes.preset_mode; + const oldPresetMode = this._stateObj!.attributes.preset_mode; if (presetMode === oldPresetMode) return; @@ -108,7 +133,7 @@ class HuiClimatePresetModesCardFeature private async _setMode(mode: string) { await this.hass!.callService("climate", "set_preset_mode", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, preset_mode: mode, }); } @@ -117,13 +142,14 @@ class HuiClimatePresetModesCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsClimatePresetModesCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsClimatePresetModesCardFeature(this.hass, this.context) ) { return null; } - const stateObj = this.stateObj; + const stateObj = this._stateObj; const options = filterModes( stateObj.attributes.preset_modes, @@ -131,7 +157,7 @@ class HuiClimatePresetModesCardFeature ).map((mode) => ({ value: mode, label: this.hass!.formatEntityAttributeValue( - this.stateObj!, + this._stateObj!, "preset_mode", mode ), @@ -155,7 +181,7 @@ class HuiClimatePresetModesCardFeature stateObj, "preset_mode" )} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} > `; @@ -167,7 +193,7 @@ class HuiClimatePresetModesCardFeature hide-label .label=${this.hass!.formatEntityAttributeName(stateObj, "preset_mode")} .value=${this._currentPresetMode} - .disabled=${this.stateObj.state === UNAVAILABLE} + .disabled=${this._stateObj.state === UNAVAILABLE} fixedMenuPosition naturalMenuWidth @selected=${this._valueChanged} diff --git a/src/panels/lovelace/card-features/hui-climate-swing-horizontal-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-swing-horizontal-modes-card-feature.ts index f6579175f8..5b7c5dd127 100644 --- a/src/panels/lovelace/card-features/hui-climate-swing-horizontal-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-swing-horizontal-modes-card-feature.ts @@ -1,5 +1,4 @@ import { mdiArrowOscillating } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -19,11 +18,19 @@ import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; import { filterModes } from "./common/filter-modes"; -import type { ClimateSwingHorizontalModesCardFeatureConfig } from "./types"; +import type { + ClimateSwingHorizontalModesCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; export const supportsClimateSwingHorizontalModesCardFeature = ( - stateObj: HassEntity + hass: HomeAssistant, + context: LovelaceCardFeatureContext ) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return ( domain === "climate" && @@ -38,7 +45,7 @@ class HuiClimateSwingHorizontalModesCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: ClimateEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: ClimateSwingHorizontalModesCardFeatureConfig; @@ -47,6 +54,15 @@ class HuiClimateSwingHorizontalModesCardFeature @query("ha-control-select-menu", true) private _haSelect?: HaControlSelectMenu; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | ClimateEntity + | undefined; + } + static getStubConfig(): ClimateSwingHorizontalModesCardFeatureConfig { return { type: "climate-swing-horizontal-modes", @@ -72,9 +88,16 @@ class HuiClimateSwingHorizontalModesCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentSwingHorizontalMode = - this.stateObj.attributes.swing_horizontal_mode; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentSwingHorizontalMode = + this._stateObj.attributes.swing_horizontal_mode; + } } } @@ -97,7 +120,7 @@ class HuiClimateSwingHorizontalModesCardFeature (ev.detail as any).value ?? ((ev.target as any).value as string); const oldSwingHorizontalMode = - this.stateObj!.attributes.swing_horizontal_mode; + this._stateObj!.attributes.swing_horizontal_mode; if (swingHorizontalMode === oldSwingHorizontalMode) return; @@ -112,7 +135,7 @@ class HuiClimateSwingHorizontalModesCardFeature private async _setMode(mode: string) { await this.hass!.callService("climate", "set_swing_horizontal_mode", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, swing_horizontal_mode: mode, }); } @@ -121,13 +144,14 @@ class HuiClimateSwingHorizontalModesCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsClimateSwingHorizontalModesCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsClimateSwingHorizontalModesCardFeature(this.hass, this.context) ) { return null; } - const stateObj = this.stateObj; + const stateObj = this._stateObj; const options = filterModes( stateObj.attributes.swing_horizontal_modes, @@ -135,7 +159,7 @@ class HuiClimateSwingHorizontalModesCardFeature ).map((mode) => ({ value: mode, label: this.hass!.formatEntityAttributeValue( - this.stateObj!, + this._stateObj!, "swing_horizontal_mode", mode ), @@ -159,7 +183,7 @@ class HuiClimateSwingHorizontalModesCardFeature stateObj, "swing_horizontal_mode" )} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} > `; @@ -174,7 +198,7 @@ class HuiClimateSwingHorizontalModesCardFeature "swing_horizontal_mode" )} .value=${this._currentSwingHorizontalMode} - .disabled=${this.stateObj.state === UNAVAILABLE} + .disabled=${this._stateObj.state === UNAVAILABLE} fixedMenuPosition naturalMenuWidth @selected=${this._valueChanged} diff --git a/src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts index c2d68431c3..df16f90064 100644 --- a/src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts @@ -1,5 +1,4 @@ import { mdiArrowOscillating } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -19,9 +18,19 @@ import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; import { filterModes } from "./common/filter-modes"; -import type { ClimateSwingModesCardFeatureConfig } from "./types"; +import type { + ClimateSwingModesCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsClimateSwingModesCardFeature = (stateObj: HassEntity) => { +export const supportsClimateSwingModesCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return ( domain === "climate" && @@ -36,7 +45,7 @@ class HuiClimateSwingModesCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: ClimateEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: ClimateSwingModesCardFeatureConfig; @@ -45,6 +54,15 @@ class HuiClimateSwingModesCardFeature @query("ha-control-select-menu", true) private _haSelect?: HaControlSelectMenu; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | ClimateEntity + | undefined; + } + static getStubConfig(): ClimateSwingModesCardFeatureConfig { return { type: "climate-swing-modes", @@ -70,8 +88,15 @@ class HuiClimateSwingModesCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentSwingMode = this.stateObj.attributes.swing_mode; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentSwingMode = this._stateObj.attributes.swing_mode; + } } } @@ -93,7 +118,7 @@ class HuiClimateSwingModesCardFeature const swingMode = (ev.detail as any).value ?? ((ev.target as any).value as string); - const oldSwingMode = this.stateObj!.attributes.swing_mode; + const oldSwingMode = this._stateObj!.attributes.swing_mode; if (swingMode === oldSwingMode) return; @@ -108,7 +133,7 @@ class HuiClimateSwingModesCardFeature private async _setMode(mode: string) { await this.hass!.callService("climate", "set_swing_mode", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, swing_mode: mode, }); } @@ -117,13 +142,14 @@ class HuiClimateSwingModesCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsClimateSwingModesCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsClimateSwingModesCardFeature(this.hass, this.context) ) { return null; } - const stateObj = this.stateObj; + const stateObj = this._stateObj; const options = filterModes( stateObj.attributes.swing_modes, @@ -131,7 +157,7 @@ class HuiClimateSwingModesCardFeature ).map((mode) => ({ value: mode, label: this.hass!.formatEntityAttributeValue( - this.stateObj!, + this._stateObj!, "swing_mode", mode ), @@ -155,7 +181,7 @@ class HuiClimateSwingModesCardFeature stateObj, "swing_mode" )} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} > `; @@ -167,7 +193,7 @@ class HuiClimateSwingModesCardFeature hide-label .label=${this.hass!.formatEntityAttributeName(stateObj, "swing_mode")} .value=${this._currentSwingMode} - .disabled=${this.stateObj.state === UNAVAILABLE} + .disabled=${this._stateObj.state === UNAVAILABLE} fixedMenuPosition naturalMenuWidth @selected=${this._valueChanged} diff --git a/src/panels/lovelace/card-features/hui-counter-actions-card-feature.ts b/src/panels/lovelace/card-features/hui-counter-actions-card-feature.ts index d4373a6827..896b4e3aea 100644 --- a/src/panels/lovelace/card-features/hui-counter-actions-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-counter-actions-card-feature.ts @@ -1,19 +1,30 @@ -import { mdiRestore, mdiPlus, mdiMinus } from "@mdi/js"; +import { mdiMinus, mdiPlus, mdiRestore } from "@mdi/js"; import type { HassEntity } from "home-assistant-js-websocket"; import type { TemplateResult } from "lit"; import { LitElement, html } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; +import "../../../components/ha-control-button"; +import "../../../components/ha-control-button-group"; import "../../../components/ha-control-select"; import { UNAVAILABLE } from "../../../data/entity"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import { COUNTER_ACTIONS, type CounterActionsCardFeatureConfig } from "./types"; -import "../../../components/ha-control-button-group"; -import "../../../components/ha-control-button"; +import { + COUNTER_ACTIONS, + type CounterActionsCardFeatureConfig, + type LovelaceCardFeatureContext, +} from "./types"; -export const supportsCounterActionsCardFeature = (stateObj: HassEntity) => { +export const supportsCounterActionsCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return domain === "counter"; }; @@ -56,10 +67,17 @@ class HuiCounterActionsCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: CounterActionsCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as HassEntity | undefined; + } + public static async getConfigElement(): Promise { await import( "../editor/config-elements/hui-counter-actions-card-feature-editor" @@ -85,8 +103,9 @@ class HuiCounterActionsCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsCounterActionsCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsCounterActionsCardFeature(this.hass, this.context) ) { return null; } @@ -96,7 +115,7 @@ class HuiCounterActionsCardFeature ${this._config?.actions ?.filter((action) => COUNTER_ACTIONS.includes(action)) .map((action) => { - const button = COUNTER_ACTIONS_BUTTON[action](this.stateObj!); + const button = COUNTER_ACTIONS_BUTTON[action](this._stateObj!); return html` @@ -120,7 +139,7 @@ class HuiCounterActionsCardFeature ev.stopPropagation(); const entry = (ev.target! as any).entry as CounterButton; this.hass!.callService("counter", entry.serviceName, { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, }); } diff --git a/src/panels/lovelace/card-features/hui-cover-open-close-card-feature.ts b/src/panels/lovelace/card-features/hui-cover-open-close-card-feature.ts index 0b4ec5e5f3..e462c06522 100644 --- a/src/panels/lovelace/card-features/hui-cover-open-close-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-cover-open-close-card-feature.ts @@ -1,5 +1,4 @@ import { mdiStop } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; @@ -9,20 +8,31 @@ import { } from "../../../common/entity/cover_icon"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-control-button"; -import "../../../components/ha-svg-icon"; import "../../../components/ha-control-button-group"; +import "../../../components/ha-svg-icon"; import { canClose, canOpen, canStop, CoverEntityFeature, + type CoverEntity, } from "../../../data/cover"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { CoverOpenCloseCardFeatureConfig } from "./types"; +import type { + CoverOpenCloseCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsCoverOpenCloseCardFeature = (stateObj: HassEntity) => { +export const supportsCoverOpenCloseCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return ( domain === "cover" && @@ -38,10 +48,17 @@ class HuiCoverOpenCloseCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: CoverOpenCloseCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as CoverEntity | undefined; + } + static getStubConfig(): CoverOpenCloseCardFeatureConfig { return { type: "cover-open-close", @@ -58,21 +75,21 @@ class HuiCoverOpenCloseCardFeature private _onOpenTap(ev): void { ev.stopPropagation(); this.hass!.callService("cover", "open_cover", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, }); } private _onCloseTap(ev): void { ev.stopPropagation(); this.hass!.callService("cover", "close_cover", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, }); } private _onStopTap(ev): void { ev.stopPropagation(); this.hass!.callService("cover", "stop_cover", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, }); } @@ -80,47 +97,48 @@ class HuiCoverOpenCloseCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsCoverOpenCloseCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsCoverOpenCloseCardFeature(this.hass, this.context) ) { return nothing; } return html` - ${supportsFeature(this.stateObj, CoverEntityFeature.OPEN) + ${supportsFeature(this._stateObj, CoverEntityFeature.OPEN) ? html` ` : nothing} - ${supportsFeature(this.stateObj, CoverEntityFeature.STOP) + ${supportsFeature(this._stateObj, CoverEntityFeature.STOP) ? html` ` : nothing} - ${supportsFeature(this.stateObj, CoverEntityFeature.CLOSE) + ${supportsFeature(this._stateObj, CoverEntityFeature.CLOSE) ? html` ` diff --git a/src/panels/lovelace/card-features/hui-cover-position-card-feature.ts b/src/panels/lovelace/card-features/hui-cover-position-card-feature.ts index 3fd595e447..81731ef8f0 100644 --- a/src/panels/lovelace/card-features/hui-cover-position-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-cover-position-card-feature.ts @@ -1,4 +1,3 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; @@ -8,16 +7,26 @@ import { computeDomain } from "../../../common/entity/compute_domain"; import { stateActive } from "../../../common/entity/state_active"; import { stateColorCss } from "../../../common/entity/state_color"; import { supportsFeature } from "../../../common/entity/supports-feature"; -import { CoverEntityFeature } from "../../../data/cover"; +import "../../../components/ha-control-slider"; +import { CoverEntityFeature, type CoverEntity } from "../../../data/cover"; import { UNAVAILABLE } from "../../../data/entity"; import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity_attributes"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { CoverPositionCardFeatureConfig } from "./types"; -import "../../../components/ha-control-slider"; +import type { + CoverPositionCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsCoverPositionCardFeature = (stateObj: HassEntity) => { +export const supportsCoverPositionCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return ( domain === "cover" && @@ -32,12 +41,19 @@ class HuiCoverPositionCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @property({ attribute: false }) public color?: string; @state() private _config?: CoverPositionCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as CoverEntity | undefined; + } + static getStubConfig(): CoverPositionCardFeatureConfig { return { type: "cover-position", @@ -55,23 +71,24 @@ class HuiCoverPositionCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsCoverPositionCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsCoverPositionCardFeature(this.hass, this.context) ) { return nothing; } - const percentage = stateActive(this.stateObj) - ? (this.stateObj.attributes.current_position ?? 0) + const percentage = stateActive(this._stateObj) + ? (this._stateObj.attributes.current_position ?? 0) : 0; const value = Math.max(Math.round(percentage), 0); - const openColor = stateColorCss(this.stateObj, "open"); + const openColor = stateColorCss(this._stateObj, "open"); const color = this.color ? computeCssColor(this.color) - : stateColorCss(this.stateObj); + : stateColorCss(this._stateObj); const style = { "--feature-color": color, @@ -91,11 +108,11 @@ class HuiCoverPositionCardFeature @value-changed=${this._valueChanged} .ariaLabel=${computeAttributeNameDisplay( this.hass.localize, - this.stateObj, + this._stateObj, this.hass.entities, "current_position" )} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} .unit=${DOMAIN_ATTRIBUTES_UNITS.cover.current_position} .locale=${this.hass.locale} > @@ -107,7 +124,7 @@ class HuiCoverPositionCardFeature if (isNaN(value)) return; this.hass!.callService("cover", "set_cover_position", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, position: value, }); } diff --git a/src/panels/lovelace/card-features/hui-cover-tilt-card-feature.ts b/src/panels/lovelace/card-features/hui-cover-tilt-card-feature.ts index 9c637bb192..0d18965beb 100644 --- a/src/panels/lovelace/card-features/hui-cover-tilt-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-cover-tilt-card-feature.ts @@ -1,24 +1,34 @@ import { mdiArrowBottomLeft, mdiArrowTopRight, mdiStop } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import { LitElement, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-control-button"; -import "../../../components/ha-svg-icon"; import "../../../components/ha-control-button-group"; +import "../../../components/ha-svg-icon"; import { CoverEntityFeature, canCloseTilt, canOpenTilt, canStopTilt, + type CoverEntity, } from "../../../data/cover"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { CoverTiltCardFeatureConfig } from "./types"; +import type { + CoverTiltCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsCoverTiltCardFeature = (stateObj: HassEntity) => { +export const supportsCoverTiltCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return ( domain === "cover" && @@ -34,10 +44,17 @@ class HuiCoverTiltCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: CoverTiltCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as CoverEntity | undefined; + } + static getStubConfig(): CoverTiltCardFeatureConfig { return { type: "cover-tilt", @@ -54,21 +71,21 @@ class HuiCoverTiltCardFeature private _onOpenTap(ev): void { ev.stopPropagation(); this.hass!.callService("cover", "open_cover_tilt", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, }); } private _onCloseTap(ev): void { ev.stopPropagation(); this.hass!.callService("cover", "close_cover_tilt", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, }); } private _onStopTap(ev): void { ev.stopPropagation(); this.hass!.callService("cover", "stop_cover_tilt", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, }); } @@ -76,42 +93,43 @@ class HuiCoverTiltCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsCoverTiltCardFeature + !this.context || + !this._stateObj || + !supportsCoverTiltCardFeature(this.hass, this.context) ) { return nothing; } return html` - ${supportsFeature(this.stateObj, CoverEntityFeature.OPEN_TILT) + ${supportsFeature(this._stateObj, CoverEntityFeature.OPEN_TILT) ? html` ` : nothing} - ${supportsFeature(this.stateObj, CoverEntityFeature.STOP_TILT) + ${supportsFeature(this._stateObj, CoverEntityFeature.STOP_TILT) ? html` ` : nothing} - ${supportsFeature(this.stateObj, CoverEntityFeature.CLOSE_TILT) + ${supportsFeature(this._stateObj, CoverEntityFeature.CLOSE_TILT) ? html` diff --git a/src/panels/lovelace/card-features/hui-cover-tilt-position-card-feature.ts b/src/panels/lovelace/card-features/hui-cover-tilt-position-card-feature.ts index 2b4ed1e2bd..15c4a5f154 100644 --- a/src/panels/lovelace/card-features/hui-cover-tilt-position-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-cover-tilt-position-card-feature.ts @@ -1,4 +1,3 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; @@ -15,11 +14,21 @@ import { generateTiltSliderTrackBackgroundGradient } from "../../../state-contro import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { CoverTiltPositionCardFeatureConfig } from "./types"; +import type { + CoverTiltPositionCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; const GRADIENT = generateTiltSliderTrackBackgroundGradient(); -export const supportsCoverTiltPositionCardFeature = (stateObj: HassEntity) => { +export const supportsCoverTiltPositionCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return ( domain === "cover" && @@ -34,12 +43,19 @@ class HuiCoverTiltPositionCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: CoverEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @property({ attribute: false }) public color?: string; @state() private _config?: CoverTiltPositionCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as CoverEntity | undefined; + } + static getStubConfig(): CoverTiltPositionCardFeatureConfig { return { type: "cover-tilt-position", @@ -57,21 +73,22 @@ class HuiCoverTiltPositionCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsCoverTiltPositionCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsCoverTiltPositionCardFeature(this.hass, this.context) ) { return nothing; } - const percentage = this.stateObj.attributes.current_tilt_position ?? 0; + const percentage = this._stateObj.attributes.current_tilt_position ?? 0; const value = Math.max(Math.round(percentage), 0); - const openColor = stateColorCss(this.stateObj, "open"); + const openColor = stateColorCss(this._stateObj, "open"); const color = this.color ? computeCssColor(this.color) - : stateColorCss(this.stateObj); + : stateColorCss(this._stateObj); const style = { "--feature-color": color, @@ -90,11 +107,11 @@ class HuiCoverTiltPositionCardFeature @value-changed=${this._valueChanged} .ariaLabel=${computeAttributeNameDisplay( this.hass.localize, - this.stateObj, + this._stateObj, this.hass.entities, "current_tilt_position" )} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} .unit=${DOMAIN_ATTRIBUTES_UNITS.cover.current_tilt_position} .locale=${this.hass.locale} > @@ -108,7 +125,7 @@ class HuiCoverTiltPositionCardFeature if (isNaN(value)) return; this.hass!.callService("cover", "set_cover_tilt_position", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, tilt_position: value, }); } diff --git a/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts index 5abdbc128c..bd70400778 100644 --- a/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts @@ -1,5 +1,4 @@ import { mdiTuneVariant } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -19,9 +18,19 @@ import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; import { filterModes } from "./common/filter-modes"; -import type { FanPresetModesCardFeatureConfig } from "./types"; +import type { + FanPresetModesCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsFanPresetModesCardFeature = (stateObj: HassEntity) => { +export const supportsFanPresetModesCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return ( domain === "fan" && supportsFeature(stateObj, FanEntityFeature.PRESET_MODE) @@ -35,7 +44,7 @@ class HuiFanPresetModesCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: FanEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: FanPresetModesCardFeatureConfig; @@ -44,6 +53,13 @@ class HuiFanPresetModesCardFeature @query("ha-control-select-menu", true) private _haSelect?: HaControlSelectMenu; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as FanEntity | undefined; + } + static getStubConfig(): FanPresetModesCardFeatureConfig { return { type: "fan-preset-modes", @@ -66,9 +82,15 @@ class HuiFanPresetModesCardFeature } protected willUpdate(changedProp: PropertyValues): void { - super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentPresetMode = this.stateObj.attributes.preset_mode; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentPresetMode = this._stateObj.attributes.preset_mode; + } } } @@ -90,7 +112,7 @@ class HuiFanPresetModesCardFeature const presetMode = (ev.detail as any).value ?? ((ev.target as any).value as string); - const oldPresetMode = this.stateObj!.attributes.preset_mode; + const oldPresetMode = this._stateObj!.attributes.preset_mode; if (presetMode === oldPresetMode) return; @@ -105,7 +127,7 @@ class HuiFanPresetModesCardFeature private async _setMode(mode: string) { await this.hass!.callService("fan", "set_preset_mode", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, preset_mode: mode, }); } @@ -114,13 +136,14 @@ class HuiFanPresetModesCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsFanPresetModesCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsFanPresetModesCardFeature(this.hass, this.context) ) { return null; } - const stateObj = this.stateObj; + const stateObj = this._stateObj; const options = filterModes( stateObj.attributes.preset_modes, @@ -128,7 +151,7 @@ class HuiFanPresetModesCardFeature ).map((mode) => ({ value: mode, label: this.hass!.formatEntityAttributeValue( - this.stateObj!, + this._stateObj!, "preset_mode", mode ), @@ -152,7 +175,7 @@ class HuiFanPresetModesCardFeature stateObj, "preset_mode" )} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} > `; @@ -164,7 +187,7 @@ class HuiFanPresetModesCardFeature hide-label .label=${this.hass!.formatEntityAttributeName(stateObj, "preset_mode")} .value=${this._currentPresetMode} - .disabled=${this.stateObj.state === UNAVAILABLE} + .disabled=${this._stateObj.state === UNAVAILABLE} fixedMenuPosition naturalMenuWidth @selected=${this._valueChanged} diff --git a/src/panels/lovelace/card-features/hui-fan-speed-card-feature.ts b/src/panels/lovelace/card-features/hui-fan-speed-card-feature.ts index 0be0146872..a809a124ee 100644 --- a/src/panels/lovelace/card-features/hui-fan-speed-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-fan-speed-card-feature.ts @@ -1,4 +1,3 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display"; @@ -9,6 +8,7 @@ import "../../../components/ha-control-select"; import type { ControlSelectOption } from "../../../components/ha-control-select"; import "../../../components/ha-control-slider"; import { UNAVAILABLE } from "../../../data/entity"; +import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity_attributes"; import type { FanEntity, FanSpeed } from "../../../data/fan"; import { computeFanSpeedCount, @@ -21,11 +21,20 @@ import { } from "../../../data/fan"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; -import type { FanSpeedCardFeatureConfig } from "./types"; -import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity_attributes"; import { cardFeatureStyles } from "./common/card-feature-styles"; +import type { + FanSpeedCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsFanSpeedCardFeature = (stateObj: HassEntity) => { +export const supportsFanSpeedCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return ( domain === "fan" && supportsFeature(stateObj, FanEntityFeature.SET_SPEED) @@ -36,10 +45,17 @@ export const supportsFanSpeedCardFeature = (stateObj: HassEntity) => { class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: FanEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: FanSpeedCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as FanEntity | undefined; + } + static getStubConfig(): FanSpeedCardFeatureConfig { return { type: "fan-speed", @@ -55,7 +71,7 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature { private _localizeSpeed(speed: FanSpeed) { if (speed === "on" || speed === "off") { - return this.hass!.formatEntityState(this.stateObj!, speed); + return this.hass!.formatEntityState(this._stateObj!, speed); } return this.hass!.localize(`ui.card.fan.speed.${speed}`) || speed; } @@ -64,16 +80,17 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature { if ( !this._config || !this.hass || - !this.stateObj || - !supportsFanSpeedCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsFanSpeedCardFeature(this.hass, this.context) ) { return nothing; } - const speedCount = computeFanSpeedCount(this.stateObj); + const speedCount = computeFanSpeedCount(this._stateObj); - const percentage = stateActive(this.stateObj) - ? (this.stateObj.attributes.percentage ?? 0) + const percentage = stateActive(this._stateObj) + ? (this._stateObj.attributes.percentage ?? 0) : 0; if (speedCount <= FAN_SPEED_COUNT_MAX_FOR_BUTTONS) { @@ -81,11 +98,11 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature { (speed) => ({ value: speed, label: this._localizeSpeed(speed), - path: computeFanSpeedIcon(this.stateObj!, speed), + path: computeFanSpeedIcon(this._stateObj!, speed), }) ); - const speed = fanPercentageToSpeed(this.stateObj, percentage); + const speed = fanPercentageToSpeed(this._stateObj, percentage); return html` `; @@ -112,15 +129,15 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature { .value=${value} min="0" max="100" - .step=${this.stateObj.attributes.percentage_step ?? 1} + .step=${this._stateObj.attributes.percentage_step ?? 1} @value-changed=${this._valueChanged} .ariaLabel=${computeAttributeNameDisplay( this.hass.localize, - this.stateObj, + this._stateObj, this.hass.entities, "percentage" )} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} .unit=${DOMAIN_ATTRIBUTES_UNITS.fan.percentage} .locale=${this.hass.locale} > @@ -130,10 +147,10 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature { private _speedValueChanged(ev: CustomEvent) { const speed = (ev.detail as any).value as FanSpeed; - const percentage = fanSpeedToPercentage(this.stateObj!, speed); + const percentage = fanSpeedToPercentage(this._stateObj!, speed); this.hass!.callService("fan", "set_percentage", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, percentage: percentage, }); } @@ -143,7 +160,7 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature { if (isNaN(value)) return; this.hass!.callService("fan", "set_percentage", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, percentage: value, }); } diff --git a/src/panels/lovelace/card-features/hui-humidifier-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-humidifier-modes-card-feature.ts index 357da0736d..82e4c4dc37 100644 --- a/src/panels/lovelace/card-features/hui-humidifier-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-humidifier-modes-card-feature.ts @@ -1,5 +1,4 @@ import { mdiTuneVariant } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -19,9 +18,19 @@ import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; import { filterModes } from "./common/filter-modes"; -import type { HumidifierModesCardFeatureConfig } from "./types"; +import type { + HumidifierModesCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsHumidifierModesCardFeature = (stateObj: HassEntity) => { +export const supportsHumidifierModesCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return ( domain === "humidifier" && @@ -36,12 +45,21 @@ class HuiHumidifierModesCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HumidifierEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: HumidifierModesCardFeatureConfig; @state() _currentMode?: string; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | HumidifierEntity + | undefined; + } + @query("ha-control-select-menu", true) private _haSelect?: HaControlSelectMenu; @@ -68,8 +86,15 @@ class HuiHumidifierModesCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentMode = this.stateObj.attributes.mode; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentMode = this._stateObj.attributes.mode; + } } } @@ -91,7 +116,7 @@ class HuiHumidifierModesCardFeature const mode = (ev.detail as any).value ?? ((ev.target as any).value as string); - const oldMode = this.stateObj!.attributes.mode; + const oldMode = this._stateObj!.attributes.mode; if (mode === oldMode) return; @@ -106,7 +131,7 @@ class HuiHumidifierModesCardFeature private async _setMode(mode: string) { await this.hass!.callService("humidifier", "set_mode", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, mode: mode, }); } @@ -115,13 +140,14 @@ class HuiHumidifierModesCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsHumidifierModesCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsHumidifierModesCardFeature(this.hass, this.context) ) { return null; } - const stateObj = this.stateObj; + const stateObj = this._stateObj; const options = filterModes( stateObj.attributes.available_modes, @@ -129,7 +155,7 @@ class HuiHumidifierModesCardFeature ).map((mode) => ({ value: mode, label: this.hass!.formatEntityAttributeValue( - this.stateObj!, + this._stateObj!, "mode", mode ), @@ -150,7 +176,7 @@ class HuiHumidifierModesCardFeature @value-changed=${this._valueChanged} hide-label .ariaLabel=${this.hass!.formatEntityAttributeName(stateObj, "mode")} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} > `; @@ -162,7 +188,7 @@ class HuiHumidifierModesCardFeature hide-label .label=${this.hass!.formatEntityAttributeName(stateObj, "mode")} .value=${this._currentMode} - .disabled=${this.stateObj.state === UNAVAILABLE} + .disabled=${this._stateObj.state === UNAVAILABLE} fixedMenuPosition naturalMenuWidth @selected=${this._valueChanged} diff --git a/src/panels/lovelace/card-features/hui-humidifier-toggle-card-feature.ts b/src/panels/lovelace/card-features/hui-humidifier-toggle-card-feature.ts index 575437c04d..4cff297c96 100644 --- a/src/panels/lovelace/card-features/hui-humidifier-toggle-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-humidifier-toggle-card-feature.ts @@ -1,5 +1,4 @@ import { mdiPower, mdiWaterPercent } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; import { LitElement, html } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -16,9 +15,19 @@ import type { import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { HumidifierToggleCardFeatureConfig } from "./types"; +import type { + HumidifierToggleCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsHumidifierToggleCardFeature = (stateObj: HassEntity) => { +export const supportsHumidifierToggleCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return domain === "humidifier"; }; @@ -30,12 +39,21 @@ class HuiHumidifierToggleCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HumidifierEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: HumidifierToggleCardFeatureConfig; @state() _currentState?: HumidifierState; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | HumidifierEntity + | undefined; + } + static getStubConfig(): HumidifierToggleCardFeatureConfig { return { type: "humidifier-toggle", @@ -51,17 +69,24 @@ class HuiHumidifierToggleCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentState = this.stateObj.state as HumidifierState; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentState = this._stateObj.state as HumidifierState; + } } } private async _valueChanged(ev: CustomEvent) { const newState = (ev.detail as any).value as HumidifierState; - if (newState === this.stateObj!.state) return; + if (newState === this._stateObj!.state) return; - const oldState = this.stateObj!.state as HumidifierState; + const oldState = this._stateObj!.state as HumidifierState; this._currentState = newState; try { @@ -76,7 +101,7 @@ class HuiHumidifierToggleCardFeature "humidifier", newState === "on" ? "turn_on" : "turn_off", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, } ); } @@ -85,17 +110,18 @@ class HuiHumidifierToggleCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsHumidifierToggleCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsHumidifierToggleCardFeature(this.hass, this.context) ) { return null; } - const color = stateColorCss(this.stateObj); + const color = stateColorCss(this._stateObj); const options = ["off", "on"].map((entityState) => ({ value: entityState, - label: this.hass!.formatEntityState(this.stateObj!, entityState), + label: this.hass!.formatEntityState(this._stateObj!, entityState), path: entityState === "on" ? mdiWaterPercent : mdiPower, })); @@ -109,7 +135,7 @@ class HuiHumidifierToggleCardFeature style=${styleMap({ "--control-select-color": color, })} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} > `; diff --git a/src/panels/lovelace/card-features/hui-lawn-mower-commands-card-feature.ts b/src/panels/lovelace/card-features/hui-lawn-mower-commands-card-feature.ts index 70306fd455..5b7debe756 100644 --- a/src/panels/lovelace/card-features/hui-lawn-mower-commands-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-lawn-mower-commands-card-feature.ts @@ -5,8 +5,8 @@ import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-control-button"; -import "../../../components/ha-svg-icon"; import "../../../components/ha-control-button-group"; +import "../../../components/ha-svg-icon"; import { UNAVAILABLE } from "../../../data/entity"; import type { LawnMowerEntity } from "../../../data/lawn_mower"; import { LawnMowerEntityFeature, canDock } from "../../../data/lawn_mower"; @@ -16,6 +16,7 @@ import { cardFeatureStyles } from "./common/card-feature-styles"; import type { LawnMowerCommand, LawnMowerCommandsCardFeatureConfig, + LovelaceCardFeatureContext, } from "./types"; import { LAWN_MOWER_COMMANDS } from "./types"; @@ -74,7 +75,14 @@ export const LAWN_MOWER_COMMANDS_BUTTONS: Record< }), }; -export const supportsLawnMowerCommandCardFeature = (stateObj: HassEntity) => { +export const supportsLawnMowerCommandCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return ( domain === "lawn_mower" && @@ -89,14 +97,26 @@ class HuiLawnMowerCommandCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: LawnMowerCommandsCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | LawnMowerEntity + | undefined; + } + static getStubConfig( - _, - stateObj?: HassEntity + hass: HomeAssistant, + context: LovelaceCardFeatureContext ): LawnMowerCommandsCardFeatureConfig { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; return { type: "lawn-mower-commands", commands: stateObj @@ -127,7 +147,7 @@ class HuiLawnMowerCommandCardFeature ev.stopPropagation(); const entry = (ev.target! as any).entry as LawnMowerButton; this.hass!.callService("lawn_mower", entry.serviceName, { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, }); } @@ -135,13 +155,14 @@ class HuiLawnMowerCommandCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsLawnMowerCommandCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsLawnMowerCommandCardFeature(this.hass, this.context) ) { return nothing; } - const stateObj = this.stateObj as LawnMowerEntity; + const stateObj = this._stateObj as LawnMowerEntity; return html` diff --git a/src/panels/lovelace/card-features/hui-light-brightness-card-feature.ts b/src/panels/lovelace/card-features/hui-light-brightness-card-feature.ts index ae48e0a74f..a0c2ef042a 100644 --- a/src/panels/lovelace/card-features/hui-light-brightness-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-light-brightness-card-feature.ts @@ -1,17 +1,26 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { stateActive } from "../../../common/entity/state_active"; import "../../../components/ha-control-slider"; import { UNAVAILABLE } from "../../../data/entity"; -import { lightSupportsBrightness } from "../../../data/light"; +import { lightSupportsBrightness, type LightEntity } from "../../../data/light"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { LightBrightnessCardFeatureConfig } from "./types"; +import type { + LightBrightnessCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsLightBrightnessCardFeature = (stateObj: HassEntity) => { +export const supportsLightBrightnessCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return domain === "light" && lightSupportsBrightness(stateObj); }; @@ -23,10 +32,17 @@ class HuiLightBrightnessCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: LightBrightnessCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id] as LightEntity | undefined; + } + static getStubConfig(): LightBrightnessCardFeatureConfig { return { type: "light-brightness", @@ -44,16 +60,17 @@ class HuiLightBrightnessCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsLightBrightnessCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsLightBrightnessCardFeature(this.hass, this.context) ) { return nothing; } const position = - this.stateObj.attributes.brightness != null + this._stateObj.attributes.brightness != null ? Math.max( - Math.round((this.stateObj.attributes.brightness * 100) / 255), + Math.round((this._stateObj.attributes.brightness * 100) / 255), 1 ) : undefined; @@ -63,8 +80,8 @@ class HuiLightBrightnessCardFeature .value=${position} min="1" max="100" - .showHandle=${stateActive(this.stateObj)} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .showHandle=${stateActive(this._stateObj)} + .disabled=${this._stateObj!.state === UNAVAILABLE} @value-changed=${this._valueChanged} .label=${this.hass.localize("ui.card.light.brightness")} unit="%" @@ -78,7 +95,7 @@ class HuiLightBrightnessCardFeature const value = ev.detail.value; this.hass!.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, brightness_pct: value, }); } diff --git a/src/panels/lovelace/card-features/hui-light-color-temp-card-feature.ts b/src/panels/lovelace/card-features/hui-light-color-temp-card-feature.ts index 4675b293a6..93a91a9ec2 100644 --- a/src/panels/lovelace/card-features/hui-light-color-temp-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-light-color-temp-card-feature.ts @@ -1,4 +1,3 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; @@ -12,14 +11,28 @@ import { stateActive } from "../../../common/entity/state_active"; import "../../../components/ha-control-slider"; import { UNAVAILABLE } from "../../../data/entity"; import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity_attributes"; -import { LightColorMode, lightSupportsColorMode } from "../../../data/light"; +import { + LightColorMode, + lightSupportsColorMode, + type LightEntity, +} from "../../../data/light"; import { generateColorTemperatureGradient } from "../../../dialogs/more-info/components/lights/light-color-temp-picker"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { LightColorTempCardFeatureConfig } from "./types"; +import type { + LightColorTempCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsLightColorTempCardFeature = (stateObj: HassEntity) => { +export const supportsLightColorTempCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return ( domain === "light" && @@ -34,10 +47,17 @@ class HuiLightColorTempCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: LightColorTempCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as LightEntity | undefined; + } + static getStubConfig(): LightColorTempCardFeatureConfig { return { type: "light-color-temp", @@ -55,21 +75,22 @@ class HuiLightColorTempCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsLightColorTempCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsLightColorTempCardFeature(this.hass, this.context) ) { return nothing; } const position = - this.stateObj.attributes.color_temp_kelvin != null - ? this.stateObj.attributes.color_temp_kelvin + this._stateObj.attributes.color_temp_kelvin != null + ? this._stateObj.attributes.color_temp_kelvin : undefined; const minKelvin = - this.stateObj.attributes.min_color_temp_kelvin ?? DEFAULT_MIN_KELVIN; + this._stateObj.attributes.min_color_temp_kelvin ?? DEFAULT_MIN_KELVIN; const maxKelvin = - this.stateObj.attributes.max_color_temp_kelvin ?? DEFAULT_MAX_KELVIN; + this._stateObj.attributes.max_color_temp_kelvin ?? DEFAULT_MAX_KELVIN; const gradient = this._generateTemperatureGradient(minKelvin!, maxKelvin); @@ -77,8 +98,8 @@ class HuiLightColorTempCardFeature { +export const supportsLockCommandsCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return domain === "lock"; }; @@ -30,10 +39,17 @@ class HuiLockCommandsCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: LockCommandsCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as LockEntity | undefined; + } + static getStubConfig(): LockCommandsCardFeatureConfig { return { type: "lock-commands", @@ -50,19 +66,20 @@ class HuiLockCommandsCardFeature private _onTap(ev): void { ev.stopPropagation(); const service = ev.target.dataset.service; - if (!this.hass || !this.stateObj || !service) { + if (!this.hass || !this._stateObj || !service) { return; } forwardHaptic("light"); - callProtectedLockService(this, this.hass, this.stateObj, service); + callProtectedLockService(this, this.hass, this._stateObj, service); } protected render() { if ( !this._config || !this.hass || - !this.stateObj || - !supportsLockCommandsCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsLockCommandsCardFeature(this.hass, this.context) ) { return nothing; } @@ -71,7 +88,7 @@ class HuiLockCommandsCardFeature @@ -79,7 +96,7 @@ class HuiLockCommandsCardFeature diff --git a/src/panels/lovelace/card-features/hui-lock-open-door-card-feature.ts b/src/panels/lovelace/card-features/hui-lock-open-door-card-feature.ts index e0a7bc4eff..194f198426 100644 --- a/src/panels/lovelace/card-features/hui-lock-open-door-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-lock-open-door-card-feature.ts @@ -1,10 +1,8 @@ import { mdiCheck } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import type { CSSResultGroup } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; - import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-control-button"; import "../../../components/ha-control-button-group"; @@ -12,13 +10,24 @@ import { callProtectedLockService, canOpen, LockEntityFeature, + type LockEntity, } from "../../../data/lock"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; -import type { LockOpenDoorCardFeatureConfig } from "./types"; import { cardFeatureStyles } from "./common/card-feature-styles"; +import type { + LockOpenDoorCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsLockOpenDoorCardFeature = (stateObj: HassEntity) => { +export const supportsLockOpenDoorCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return domain === "lock" && supportsFeature(stateObj, LockEntityFeature.OPEN); }; @@ -35,7 +44,7 @@ class HuiLockOpenDoorCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() public _buttonState: ButtonState = "normal"; @@ -43,6 +52,13 @@ class HuiLockOpenDoorCardFeature private _buttonTimeout?: number; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as LockEntity | undefined; + } + static getStubConfig(): LockOpenDoorCardFeatureConfig { return { type: "lock-open-door", @@ -71,10 +87,10 @@ class HuiLockOpenDoorCardFeature this._setButtonState("confirm", CONFIRM_TIMEOUT_SECOND); return; } - if (!this.hass || !this.stateObj) { + if (!this.hass || !this._stateObj) { return; } - callProtectedLockService(this, this.hass, this.stateObj!, "open"); + callProtectedLockService(this, this.hass, this._stateObj!, "open"); this._setButtonState("done", DONE_TIMEOUT_SECOND); } @@ -83,8 +99,9 @@ class HuiLockOpenDoorCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsLockOpenDoorCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsLockOpenDoorCardFeature(this.hass, this.context) ) { return nothing; } @@ -100,7 +117,7 @@ class HuiLockOpenDoorCardFeature : html` diff --git a/src/panels/lovelace/card-features/hui-media-player-volume-slider-card-feature.ts b/src/panels/lovelace/card-features/hui-media-player-volume-slider-card-feature.ts index 3aeffe2b19..c330b6e1cb 100644 --- a/src/panels/lovelace/card-features/hui-media-player-volume-slider-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-media-player-volume-slider-card-feature.ts @@ -1,20 +1,30 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { stateActive } from "../../../common/entity/state_active"; +import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-control-slider"; import { isUnavailableState } from "../../../data/entity"; +import { + MediaPlayerEntityFeature, + type MediaPlayerEntity, +} from "../../../data/media-player"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { MediaPlayerVolumeSliderCardFeatureConfig } from "./types"; -import { MediaPlayerEntityFeature } from "../../../data/media-player"; -import { supportsFeature } from "../../../common/entity/supports-feature"; +import type { + LovelaceCardFeatureContext, + MediaPlayerVolumeSliderCardFeatureConfig, +} from "./types"; export const supportsMediaPlayerVolumeSliderCardFeature = ( - stateObj: HassEntity + hass: HomeAssistant, + context: LovelaceCardFeatureContext ) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return ( domain === "media_player" && @@ -29,10 +39,19 @@ class HuiMediaPlayerVolumeSliderCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: MediaPlayerVolumeSliderCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | MediaPlayerEntity + | undefined; + } + static getStubConfig(): MediaPlayerVolumeSliderCardFeatureConfig { return { type: "media-player-volume-slider", @@ -50,15 +69,16 @@ class HuiMediaPlayerVolumeSliderCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsMediaPlayerVolumeSliderCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsMediaPlayerVolumeSliderCardFeature(this.hass, this.context) ) { return nothing; } const position = - this.stateObj.attributes.volume_level != null - ? Math.round(this.stateObj.attributes.volume_level * 100) + this._stateObj.attributes.volume_level != null + ? Math.round(this._stateObj.attributes.volume_level * 100) : undefined; return html` @@ -66,8 +86,8 @@ class HuiMediaPlayerVolumeSliderCardFeature .value=${position} min="0" max="100" - .showHandle=${stateActive(this.stateObj)} - .disabled=${!this.stateObj || isUnavailableState(this.stateObj.state)} + .showHandle=${stateActive(this._stateObj)} + .disabled=${!this._stateObj || isUnavailableState(this._stateObj.state)} @value-changed=${this._valueChanged} unit="%" .locale=${this.hass.locale} @@ -80,7 +100,7 @@ class HuiMediaPlayerVolumeSliderCardFeature const value = ev.detail.value; this.hass!.callService("media_player", "volume_set", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, volume_level: value / 100, }); } diff --git a/src/panels/lovelace/card-features/hui-numeric-input-card-feature.ts b/src/panels/lovelace/card-features/hui-numeric-input-card-feature.ts index 561ce890cf..60c9a89cdf 100644 --- a/src/panels/lovelace/card-features/hui-numeric-input-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-numeric-input-card-feature.ts @@ -12,9 +12,19 @@ import { isUnavailableState } from "../../../data/entity"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { NumericInputCardFeatureConfig } from "./types"; +import type { + LovelaceCardFeatureContext, + NumericInputCardFeatureConfig, +} from "./types"; -export const supportsNumericInputCardFeature = (stateObj: HassEntity) => { +export const supportsNumericInputCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return domain === "input_number" || domain === "number"; }; @@ -26,7 +36,7 @@ class HuiNumericInputCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: NumericInputCardFeatureConfig; @@ -39,6 +49,13 @@ class HuiNumericInputCardFeature }; } + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as HassEntity | undefined; + } + public static async getConfigElement(): Promise { await import( "../editor/config-elements/hui-numeric-input-card-feature-editor" @@ -55,13 +72,20 @@ class HuiNumericInputCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentState = this.stateObj.state; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentState = this._stateObj.state; + } } } private async _setValue(ev: CustomEvent) { - const stateObj = this.stateObj!; + const stateObj = this._stateObj!; const domain = computeDomain(stateObj.entity_id); @@ -75,13 +99,14 @@ class HuiNumericInputCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsNumericInputCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsNumericInputCardFeature(this.hass, this.context) ) { return nothing; } - const stateObj = this.stateObj; + const stateObj = this._stateObj; const parsedState = Number(stateObj.state); const value = !isNaN(parsedState) ? parsedState : undefined; diff --git a/src/panels/lovelace/card-features/hui-select-options-card-feature.ts b/src/panels/lovelace/card-features/hui-select-options-card-feature.ts index bb13f1c206..f58f80d8b4 100644 --- a/src/panels/lovelace/card-features/hui-select-options-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-select-options-card-feature.ts @@ -1,4 +1,3 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues } from "lit"; import { html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -15,9 +14,19 @@ import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; import { filterModes } from "./common/filter-modes"; -import type { SelectOptionsCardFeatureConfig } from "./types"; +import type { + LovelaceCardFeatureContext, + SelectOptionsCardFeatureConfig, +} from "./types"; -export const supportsSelectOptionsCardFeature = (stateObj: HassEntity) => { +export const supportsSelectOptionsCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return domain === "select" || domain === "input_select"; }; @@ -29,9 +38,7 @@ class HuiSelectOptionsCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: - | SelectEntity - | InputSelectEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: SelectOptionsCardFeatureConfig; @@ -40,6 +47,16 @@ class HuiSelectOptionsCardFeature @query("ha-control-select-menu", true) private _haSelect!: HaControlSelectMenu; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | SelectEntity + | InputSelectEntity + | undefined; + } + static getStubConfig(): SelectOptionsCardFeatureConfig { return { type: "select-options", @@ -62,8 +79,15 @@ class HuiSelectOptionsCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentOption = this.stateObj.state; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentOption = this._stateObj.state; + } } } @@ -84,11 +108,11 @@ class HuiSelectOptionsCardFeature private async _valueChanged(ev: CustomEvent) { const option = (ev.target as any).value as string; - const oldOption = this.stateObj!.state; + const oldOption = this._stateObj!.state; if ( option === oldOption || - !this.stateObj!.attributes.options.includes(option) + !this._stateObj!.attributes.options.includes(option) ) return; @@ -102,9 +126,9 @@ class HuiSelectOptionsCardFeature } private async _setOption(option: string) { - const domain = computeDomain(this.stateObj!.entity_id); + const domain = computeDomain(this._stateObj!.entity_id); await this.hass!.callService(domain, "select_option", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, option: option, }); } @@ -113,16 +137,17 @@ class HuiSelectOptionsCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsSelectOptionsCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsSelectOptionsCardFeature(this.hass, this.context) ) { return nothing; } - const stateObj = this.stateObj; + const stateObj = this._stateObj; const options = this._getOptions( - this.stateObj.attributes.options, + this._stateObj.attributes.options, this._config.options ); @@ -133,7 +158,7 @@ class HuiSelectOptionsCardFeature .label=${this.hass.localize("ui.card.select.option")} .value=${stateObj.state} .options=${options} - .disabled=${this.stateObj.state === UNAVAILABLE} + .disabled=${this._stateObj.state === UNAVAILABLE} fixedMenuPosition naturalMenuWidth @selected=${this._valueChanged} diff --git a/src/panels/lovelace/card-features/hui-target-humidity-card-feature.ts b/src/panels/lovelace/card-features/hui-target-humidity-card-feature.ts index f4650a66ad..1bef28543d 100644 --- a/src/panels/lovelace/card-features/hui-target-humidity-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-target-humidity-card-feature.ts @@ -1,4 +1,3 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues } from "lit"; import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -9,9 +8,19 @@ import type { HumidifierEntity } from "../../../data/humidifier"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { TargetHumidityCardFeatureConfig } from "./types"; +import type { + LovelaceCardFeatureContext, + TargetHumidityCardFeatureConfig, +} from "./types"; -export const supportsTargetHumidityCardFeature = (stateObj: HassEntity) => { +export const supportsTargetHumidityCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return domain === "humidifier"; }; @@ -23,12 +32,21 @@ class HuiTargetHumidityCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HumidifierEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: TargetHumidityCardFeatureConfig; @state() private _targetHumidity?: number; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | HumidifierEntity + | undefined; + } + static getStubConfig(): TargetHumidityCardFeatureConfig { return { type: "target-humidity", @@ -44,19 +62,26 @@ class HuiTargetHumidityCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj")) { - this._targetHumidity = this.stateObj!.attributes.humidity; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._targetHumidity = this._stateObj!.attributes.humidity; + } } } private _step = 1; private get _min() { - return this.stateObj!.attributes.min_humidity ?? 0; + return this._stateObj!.attributes.min_humidity ?? 0; } private get _max() { - return this.stateObj!.attributes.max_humidity ?? 100; + return this._stateObj!.attributes.max_humidity ?? 100; } private _valueChanged(ev: CustomEvent) { @@ -68,7 +93,7 @@ class HuiTargetHumidityCardFeature private _callService() { this.hass!.callService("humidifier", "set_humidity", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, humidity: this._targetHumidity, }); } @@ -77,21 +102,25 @@ class HuiTargetHumidityCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsTargetHumidityCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsTargetHumidityCardFeature(this.hass, this.context) ) { return nothing; } return html` diff --git a/src/panels/lovelace/card-features/hui-target-temperature-card-feature.ts b/src/panels/lovelace/card-features/hui-target-temperature-card-feature.ts index 0cea86d0aa..1c7d763f93 100644 --- a/src/panels/lovelace/card-features/hui-target-temperature-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-target-temperature-card-feature.ts @@ -1,4 +1,3 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues } from "lit"; import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -19,11 +18,21 @@ import { WaterHeaterEntityFeature } from "../../../data/water_heater"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { TargetTemperatureCardFeatureConfig } from "./types"; +import type { + LovelaceCardFeatureContext, + TargetTemperatureCardFeatureConfig, +} from "./types"; type Target = "value" | "low" | "high"; -export const supportsTargetTemperatureCardFeature = (stateObj: HassEntity) => { +export const supportsTargetTemperatureCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return ( (domain === "climate" && @@ -44,14 +53,22 @@ class HuiTargetTemperatureCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: - | ClimateEntity - | WaterHeaterEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: TargetTemperatureCardFeatureConfig; @state() private _targetTemperature: Partial> = {}; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | WaterHeaterEntity + | ClimateEntity + | undefined; + } + static getStubConfig(): TargetTemperatureCardFeatureConfig { return { type: "target-temperature", @@ -67,34 +84,41 @@ class HuiTargetTemperatureCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj")) { - this._targetTemperature = { - value: this.stateObj!.attributes.temperature, - low: - "target_temp_low" in this.stateObj!.attributes - ? this.stateObj!.attributes.target_temp_low - : undefined, - high: - "target_temp_high" in this.stateObj!.attributes - ? this.stateObj!.attributes.target_temp_high - : undefined, - }; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._targetTemperature = { + value: this._stateObj!.attributes.temperature, + low: + "target_temp_low" in this._stateObj!.attributes + ? this._stateObj!.attributes.target_temp_low + : undefined, + high: + "target_temp_high" in this._stateObj!.attributes + ? this._stateObj!.attributes.target_temp_high + : undefined, + }; + } } } private get _step() { return ( - this.stateObj!.attributes.target_temp_step || + this._stateObj!.attributes.target_temp_step || (this.hass!.config.unit_system.temperature === UNIT_F ? 1 : 0.5) ); } private get _min() { - return this.stateObj!.attributes.min_temp; + return this._stateObj!.attributes.min_temp; } private get _max() { - return this.stateObj!.attributes.max_temp; + return this._stateObj!.attributes.max_temp; } private async _valueChanged(ev: CustomEvent) { @@ -115,43 +139,43 @@ class HuiTargetTemperatureCardFeature ); private _callService(type: string) { - const domain = computeStateDomain(this.stateObj!); + const domain = computeStateDomain(this._stateObj!); if (type === "high" || type === "low") { this.hass!.callService(domain, "set_temperature", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, target_temp_low: this._targetTemperature.low, target_temp_high: this._targetTemperature.high, }); return; } this.hass!.callService(domain, "set_temperature", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, temperature: this._targetTemperature.value, }); } private _supportsTarget() { - const domain = computeStateDomain(this.stateObj!); + const domain = computeStateDomain(this._stateObj!); return ( (domain === "climate" && supportsFeature( - this.stateObj!, + this._stateObj!, ClimateEntityFeature.TARGET_TEMPERATURE )) || (domain === "water_heater" && supportsFeature( - this.stateObj!, + this._stateObj!, WaterHeaterEntityFeature.TARGET_TEMPERATURE )) ); } private _supportsTargetRange() { - const domain = computeStateDomain(this.stateObj!); + const domain = computeStateDomain(this._stateObj!); return ( domain === "climate" && supportsFeature( - this.stateObj!, + this._stateObj!, ClimateEntityFeature.TARGET_TEMPERATURE_RANGE ) ); @@ -161,13 +185,14 @@ class HuiTargetTemperatureCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsTargetTemperatureCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsTargetTemperatureCardFeature(this.hass, this.context) ) { return nothing; } - const stateColor = stateColorCss(this.stateObj); + const stateColor = stateColorCss(this._stateObj); const digits = this._step.toString().split(".")?.[1]?.length ?? 0; const options = { @@ -178,27 +203,27 @@ class HuiTargetTemperatureCardFeature if ( this._supportsTarget() && this._targetTemperature.value != null && - this.stateObj.state !== UNAVAILABLE + this._stateObj.state !== UNAVAILABLE ) { return html` @@ -210,7 +235,7 @@ class HuiTargetTemperatureCardFeature this._supportsTargetRange() && this._targetTemperature.low != null && this._targetTemperature.high != null && - this.stateObj.state !== UNAVAILABLE + this._stateObj.state !== UNAVAILABLE ) { return html` @@ -227,13 +252,13 @@ class HuiTargetTemperatureCardFeature .step=${this._step} @value-changed=${this._valueChanged} .label=${this.hass.formatEntityAttributeName( - this.stateObj, + this._stateObj, "target_temp_low" )} style=${styleMap({ "--control-number-buttons-focus-color": stateColor, })} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} .locale=${this.hass.locale} > @@ -250,13 +275,13 @@ class HuiTargetTemperatureCardFeature .step=${this._step} @value-changed=${this._valueChanged} .label=${this.hass.formatEntityAttributeName( - this.stateObj, + this._stateObj, "target_temp_high" )} style=${styleMap({ "--control-number-buttons-focus-color": stateColor, })} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} .locale=${this.hass.locale} > @@ -267,10 +292,10 @@ class HuiTargetTemperatureCardFeature return html` { +export const supportsToggleCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return [ "switch", @@ -56,10 +66,17 @@ const DOMAIN_ICONS: Record = { class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: ToggleCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as HassEntity | undefined; + } + static getStubConfig(): ToggleCardFeatureConfig { return { type: "toggle", @@ -92,16 +109,16 @@ class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature { } private async _callService(turnOn): Promise { - if (!this.hass || !this.stateObj) { + if (!this.hass || !this._stateObj) { return; } forwardHaptic("light"); - const stateDomain = computeDomain(this.stateObj.entity_id); + const stateDomain = computeDomain(this._stateObj.entity_id); const serviceDomain = stateDomain; const service = turnOn ? "turn_on" : "turn_off"; await this.hass.callService(serviceDomain, service, { - entity_id: this.stateObj.entity_id, + entity_id: this._stateObj.entity_id, }); } @@ -109,32 +126,33 @@ class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature { if ( !this._config || !this.hass || - !this.stateObj || - !supportsToggleCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsToggleCardFeature(this.hass, this.context) ) { return nothing; } const onColor = "var(--feature-color)"; - const offColor = stateColorCss(this.stateObj, "off"); + const offColor = stateColorCss(this._stateObj, "off"); - const isOn = this.stateObj.state === "on"; - const isOff = this.stateObj.state === "off"; + const isOn = this._stateObj.state === "on"; + const isOff = this._stateObj.state === "off"; - const domain = computeDomain(this.stateObj.entity_id); + const domain = computeDomain(this._stateObj.entity_id); const onIcon = DOMAIN_ICONS[domain]?.on || mdiPower; const offIcon = DOMAIN_ICONS[domain]?.off || mdiPowerOff; if ( - this.stateObj.attributes.assumed_state || - this.stateObj.state === UNKNOWN + this._stateObj.attributes.assumed_state || + this._stateObj.state === UNKNOWN ) { return html` `; diff --git a/src/panels/lovelace/card-features/hui-update-actions-card-feature.ts b/src/panels/lovelace/card-features/hui-update-actions-card-feature.ts index 1956df515a..80383e30eb 100644 --- a/src/panels/lovelace/card-features/hui-update-actions-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-update-actions-card-feature.ts @@ -1,5 +1,4 @@ import { mdiCancel, mdiCellphoneArrowDown } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import { LitElement, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; @@ -14,11 +13,21 @@ import { showUpdateBackupDialogParams } from "../../../dialogs/update_backup/sho import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { UpdateActionsCardFeatureConfig } from "./types"; +import type { + LovelaceCardFeatureContext, + UpdateActionsCardFeatureConfig, +} from "./types"; export const DEFAULT_UPDATE_BACKUP_OPTION = "no"; -export const supportsUpdateActionsCardFeature = (stateObj: HassEntity) => { +export const supportsUpdateActionsCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return ( domain === "update" && @@ -33,10 +42,19 @@ class HuiUpdateActionsCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: UpdateActionsCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | UpdateEntity + | undefined; + } + public static async getConfigElement(): Promise { await import( "../editor/config-elements/hui-update-actions-card-feature-editor" @@ -59,7 +77,7 @@ class HuiUpdateActionsCardFeature } private get _installDisabled(): boolean { - const stateObj = this.stateObj as UpdateEntity; + const stateObj = this._stateObj as UpdateEntity; if (stateObj.state === UNAVAILABLE) return true; @@ -74,7 +92,7 @@ class HuiUpdateActionsCardFeature } private get _skipDisabled(): boolean { - const stateObj = this.stateObj as UpdateEntity; + const stateObj = this._stateObj as UpdateEntity; if (stateObj.state === UNAVAILABLE) return true; @@ -89,7 +107,7 @@ class HuiUpdateActionsCardFeature private async _install(): Promise { const supportsBackup = supportsFeature( - this.stateObj!, + this._stateObj!, UpdateEntityFeature.BACKUP ); let backup = supportsBackup && this._config?.backup === "yes"; @@ -101,14 +119,14 @@ class HuiUpdateActionsCardFeature } this.hass!.callService("update", "install", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, backup: backup, }); } private async _skip(): Promise { this.hass!.callService("update", "skip", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, }); } @@ -116,8 +134,9 @@ class HuiUpdateActionsCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsUpdateActionsCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsUpdateActionsCardFeature(this.hass, this.context) ) { return nothing; } diff --git a/src/panels/lovelace/card-features/hui-vacuum-commands-card-feature.ts b/src/panels/lovelace/card-features/hui-vacuum-commands-card-feature.ts index 20f77d12ca..433993ab21 100644 --- a/src/panels/lovelace/card-features/hui-vacuum-commands-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-vacuum-commands-card-feature.ts @@ -13,8 +13,8 @@ import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-control-button"; -import "../../../components/ha-svg-icon"; import "../../../components/ha-control-button-group"; +import "../../../components/ha-svg-icon"; import { UNAVAILABLE } from "../../../data/entity"; import type { VacuumEntity } from "../../../data/vacuum"; import { @@ -27,7 +27,11 @@ import { import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { VacuumCommand, VacuumCommandsCardFeatureConfig } from "./types"; +import type { + LovelaceCardFeatureContext, + VacuumCommand, + VacuumCommandsCardFeatureConfig, +} from "./types"; import { VACUUM_COMMANDS } from "./types"; interface VacuumButton { @@ -115,7 +119,14 @@ export const VACUUM_COMMANDS_BUTTONS: Record< }), }; -export const supportsVacuumCommandsCardFeature = (stateObj: HassEntity) => { +export const supportsVacuumCommandsCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return ( domain === "vacuum" && @@ -130,14 +141,26 @@ class HuiVacuumCommandCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: VacuumCommandsCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | VacuumEntity + | undefined; + } + static getStubConfig( - _, - stateObj?: HassEntity + hass: HomeAssistant, + context: LovelaceCardFeatureContext ): VacuumCommandsCardFeatureConfig { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; return { type: "vacuum-commands", commands: stateObj @@ -166,7 +189,7 @@ class HuiVacuumCommandCardFeature ev.stopPropagation(); const entry = (ev.target! as any).entry as VacuumButton; this.hass!.callService("vacuum", entry.serviceName, { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, }); } @@ -174,13 +197,14 @@ class HuiVacuumCommandCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsVacuumCommandsCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsVacuumCommandsCardFeature(this.hass, this.context) ) { return nothing; } - const stateObj = this.stateObj as VacuumEntity; + const stateObj = this._stateObj as VacuumEntity; return html` diff --git a/src/panels/lovelace/card-features/hui-water-heater-operation-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-water-heater-operation-modes-card-feature.ts index a74041cbdd..2f61d1ac95 100644 --- a/src/panels/lovelace/card-features/hui-water-heater-operation-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-water-heater-operation-modes-card-feature.ts @@ -1,4 +1,3 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -23,11 +22,19 @@ import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; import { filterModes } from "./common/filter-modes"; -import type { WaterHeaterOperationModesCardFeatureConfig } from "./types"; +import type { + LovelaceCardFeatureContext, + WaterHeaterOperationModesCardFeatureConfig, +} from "./types"; export const supportsWaterHeaterOperationModesCardFeature = ( - stateObj: HassEntity + hass: HomeAssistant, + context: LovelaceCardFeatureContext ) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; const domain = computeDomain(stateObj.entity_id); return domain === "water_heater"; }; @@ -39,12 +46,21 @@ class HuiWaterHeaterOperationModeCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: WaterHeaterEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: WaterHeaterOperationModesCardFeatureConfig; @state() _currentOperationMode?: OperationMode; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | WaterHeaterEntity + | undefined; + } + static getStubConfig(): WaterHeaterOperationModesCardFeatureConfig { return { type: "water-heater-operation-modes", @@ -69,17 +85,24 @@ class HuiWaterHeaterOperationModeCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentOperationMode = this.stateObj.state as OperationMode; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentOperationMode = this._stateObj.state as OperationMode; + } } } private async _valueChanged(ev: CustomEvent) { const mode = (ev.detail as any).value as OperationMode; - if (mode === this.stateObj!.state) return; + if (mode === this._stateObj!.state) return; - const oldMode = this.stateObj!.state as OperationMode; + const oldMode = this._stateObj!.state as OperationMode; this._currentOperationMode = mode; try { @@ -91,7 +114,7 @@ class HuiWaterHeaterOperationModeCardFeature private async _setMode(mode: OperationMode) { await this.hass!.callService("water_heater", "set_operation_mode", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, operation_mode: mode, }); } @@ -100,15 +123,16 @@ class HuiWaterHeaterOperationModeCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsWaterHeaterOperationModesCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsWaterHeaterOperationModesCardFeature(this.hass, this.context) ) { return null; } - const color = stateColorCss(this.stateObj); + const color = stateColorCss(this._stateObj); - const orderedModes = (this.stateObj.attributes.operation_list || []) + const orderedModes = (this._stateObj.attributes.operation_list || []) .concat() .sort(compareWaterHeaterOperationMode) .reverse(); @@ -118,7 +142,7 @@ class HuiWaterHeaterOperationModeCardFeature this._config.operation_modes ).map((mode) => ({ value: mode, - label: this.hass!.formatEntityState(this.stateObj!, mode), + label: this.hass!.formatEntityState(this._stateObj!, mode), path: computeOperationModeIcon(mode as OperationMode), })); @@ -132,7 +156,7 @@ class HuiWaterHeaterOperationModeCardFeature style=${styleMap({ "--control-select-color": color, })} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} > `; diff --git a/src/panels/lovelace/cards/hui-humidifier-card.ts b/src/panels/lovelace/cards/hui-humidifier-card.ts index 5ed6ad3e3a..761369d350 100644 --- a/src/panels/lovelace/cards/hui-humidifier-card.ts +++ b/src/panels/lovelace/cards/hui-humidifier-card.ts @@ -14,6 +14,7 @@ import type { HumidifierEntity } from "../../../data/humidifier"; import "../../../state-control/humidifier/ha-state-control-humidifier-humidity"; import type { HomeAssistant } from "../../../types"; import "../card-features/hui-card-features"; +import type { LovelaceCardFeatureContext } from "../card-features/types"; import { findEntities } from "../common/find-entities"; import { createEntityNotFoundWarning } from "../components/hui-warning"; import type { @@ -69,6 +70,8 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard { @state() private _config?: HumidifierCardConfig; + @state() private _featureContext: LovelaceCardFeatureContext = {}; + public getCardSize(): number { return 7; } @@ -79,6 +82,9 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard { } this._config = config; + this._featureContext = { + entity_id: config.entity, + }; } private _handleMoreInfo() { @@ -165,7 +171,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard { "--feature-color": color, })} .hass=${this.hass} - .stateObj=${stateObj} + .context=${this._featureContext} .features=${this._config.features} >` : nothing} diff --git a/src/panels/lovelace/cards/hui-thermostat-card.ts b/src/panels/lovelace/cards/hui-thermostat-card.ts index 3e6853e405..a97e95d980 100644 --- a/src/panels/lovelace/cards/hui-thermostat-card.ts +++ b/src/panels/lovelace/cards/hui-thermostat-card.ts @@ -14,6 +14,7 @@ import type { ClimateEntity } from "../../../data/climate"; import "../../../state-control/climate/ha-state-control-climate-temperature"; import type { HomeAssistant } from "../../../types"; import "../card-features/hui-card-features"; +import type { LovelaceCardFeatureContext } from "../card-features/types"; import { findEntities } from "../common/find-entities"; import { createEntityNotFoundWarning } from "../components/hui-warning"; import type { @@ -61,6 +62,8 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { @state() private _config?: ThermostatCardConfig; + @state() private _featureContext: LovelaceCardFeatureContext = {}; + public getCardSize(): number { return 7; } @@ -71,6 +74,9 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { } this._config = config; + this._featureContext = { + entity_id: config.entity, + }; } private _handleMoreInfo() { @@ -157,7 +163,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { "--feature-color": color, })} .hass=${this.hass} - .stateObj=${stateObj} + .context=${this._featureContext} .features=${this._config.features} >` : nothing} diff --git a/src/panels/lovelace/cards/hui-tile-card.ts b/src/panels/lovelace/cards/hui-tile-card.ts index d0c48dc8b4..869ef18003 100644 --- a/src/panels/lovelace/cards/hui-tile-card.ts +++ b/src/panels/lovelace/cards/hui-tile-card.ts @@ -36,6 +36,7 @@ import type { } from "../types"; import { renderTileBadge } from "./tile/badges/tile-badge"; import type { TileCardConfig } from "./types"; +import type { LovelaceCardFeatureContext } from "../card-features/types"; import { createEntityNotFoundWarning } from "../components/hui-warning"; export const getEntityDefaultTileIconAction = (entityId: string) => { @@ -84,6 +85,8 @@ export class HuiTileCard extends LitElement implements LovelaceCard { @state() private _config?: TileCardConfig; + @state() private _featureContext: LovelaceCardFeatureContext = {}; + public setConfig(config: TileCardConfig): void { if (!config.entity) { throw new Error("Specify an entity"); @@ -98,6 +101,9 @@ export class HuiTileCard extends LitElement implements LovelaceCard { }, ...config, }; + this._featureContext = { + entity_id: config.entity, + }; } public getCardSize(): number { @@ -335,7 +341,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard { ? html` diff --git a/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts b/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts index 18ffbd4276..b92723ecc2 100644 --- a/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts @@ -1,5 +1,4 @@ import { mdiDelete, mdiDrag, mdiPencil, mdiPlus } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import { repeat } from "lit/directives/repeat"; @@ -22,8 +21,8 @@ import { supportsAlarmModesCardFeature } from "../../card-features/hui-alarm-mod import { supportsClimateFanModesCardFeature } from "../../card-features/hui-climate-fan-modes-card-feature"; import { supportsClimateHvacModesCardFeature } from "../../card-features/hui-climate-hvac-modes-card-feature"; import { supportsClimatePresetModesCardFeature } from "../../card-features/hui-climate-preset-modes-card-feature"; -import { supportsClimateSwingModesCardFeature } from "../../card-features/hui-climate-swing-modes-card-feature"; import { supportsClimateSwingHorizontalModesCardFeature } from "../../card-features/hui-climate-swing-horizontal-modes-card-feature"; +import { supportsClimateSwingModesCardFeature } from "../../card-features/hui-climate-swing-modes-card-feature"; import { supportsCounterActionsCardFeature } from "../../card-features/hui-counter-actions-card-feature"; import { supportsCoverOpenCloseCardFeature } from "../../card-features/hui-cover-open-close-card-feature"; import { supportsCoverPositionCardFeature } from "../../card-features/hui-cover-position-card-feature"; @@ -47,11 +46,18 @@ import { supportsToggleCardFeature } from "../../card-features/hui-toggle-card-f import { supportsUpdateActionsCardFeature } from "../../card-features/hui-update-actions-card-feature"; import { supportsVacuumCommandsCardFeature } from "../../card-features/hui-vacuum-commands-card-feature"; import { supportsWaterHeaterOperationModesCardFeature } from "../../card-features/hui-water-heater-operation-modes-card-feature"; -import type { LovelaceCardFeatureConfig } from "../../card-features/types"; +import type { + LovelaceCardFeatureConfig, + LovelaceCardFeatureContext, +} from "../../card-features/types"; import { getCardFeatureElementClass } from "../../create-element/create-card-feature-element"; export type FeatureType = LovelaceCardFeatureConfig["type"]; -type SupportsFeature = (stateObj: HassEntity) => boolean; + +type SupportsFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => boolean; const UI_FEATURE_TYPES = [ "alarm-modes", @@ -152,7 +158,8 @@ customCardFeatures.forEach((feature) => { }); export const getSupportedFeaturesType = ( - stateObj: HassEntity, + hass: HomeAssistant, + context: LovelaceCardFeatureContext, featuresTypes?: string[] ) => { const filteredFeaturesTypes = UI_FEATURE_TYPES.filter( @@ -164,23 +171,41 @@ export const getSupportedFeaturesType = ( ); return filteredFeaturesTypes .concat(customFeaturesTypes) - .filter((type) => supportsFeaturesType(stateObj, type)); + .filter((type) => supportsFeaturesType(hass, context, type)); }; -export const supportsFeaturesType = (stateObj: HassEntity, type: string) => { +export const supportsFeaturesType = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext, + type: string +) => { if (isCustomType(type)) { const customType = stripCustomPrefix(type); const customFeatureEntry = CUSTOM_FEATURE_ENTRIES[customType]; - if (!customFeatureEntry?.supported) return true; + + if (!customFeatureEntry) { + return false; + } try { - return customFeatureEntry.supported(stateObj); + if (customFeatureEntry.isSupported) { + return customFeatureEntry.isSupported(hass, context); + } + // Fallback to the old supported method + if (customFeatureEntry.supported) { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; + return customFeatureEntry.supported(stateObj); + } + return true; } catch { return false; } } const supportsFeature = SUPPORTS_FEATURE_TYPES[type]; - return !supportsFeature || supportsFeature(stateObj); + return !supportsFeature || supportsFeature(hass, context); }; declare global { @@ -195,7 +220,7 @@ declare global { export class HuiCardFeaturesEditor extends LitElement { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @property({ attribute: false }) public features?: LovelaceCardFeatureConfig[]; @@ -209,13 +234,17 @@ export class HuiCardFeaturesEditor extends LitElement { private _featuresKeys = new WeakMap(); private _supportsFeatureType(type: string): boolean { - if (!this.stateObj) return false; - return supportsFeaturesType(this.stateObj, type); + if (!this.hass || !this.context) return false; + return supportsFeaturesType(this.hass, this.context, type); } private _getSupportedFeaturesType() { - if (!this.stateObj) return []; - return getSupportedFeaturesType(this.stateObj, this.featuresTypes); + if (!this.hass || !this.context) return []; + return getSupportedFeaturesType( + this.hass, + this.context, + this.featuresTypes + ); } private _isFeatureTypeEditable(type: string) { @@ -288,7 +317,7 @@ export class HuiCardFeaturesEditor extends LitElement {
${this._getFeatureTypeLabel(type)} - ${this.stateObj && !supported + ${this.context && !supported ? html` ${this.hass!.localize( @@ -379,7 +408,14 @@ export class HuiCardFeaturesEditor extends LitElement { let newFeature: LovelaceCardFeatureConfig; if (elClass && elClass.getStubConfig) { - newFeature = await elClass.getStubConfig(this.hass!, this.stateObj); + try { + newFeature = await elClass.getStubConfig(this.hass!, this.context!); + } catch (_err) { + const stateObj = this.context!.entity_id + ? this.hass!.states[this.context!.entity_id] + : undefined; + newFeature = await elClass.getStubConfig(this.hass!, stateObj); + } } else { newFeature = { type: value } as LovelaceCardFeatureConfig; } diff --git a/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts index ebc035dfe3..e110fa8033 100644 --- a/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts @@ -1,6 +1,7 @@ import { mdiListBox } from "@mdi/js"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { any, array, @@ -85,13 +86,19 @@ export class HuiHumidifierCardEditor this._config = config; } + private _featureContext = memoizeOne( + (entityId?: string): LovelaceCardFeatureContext => ({ + entity_id: entityId, + }) + ); + protected render() { if (!this.hass || !this._config) { return nothing; } - const entityId = this._config!.entity; - const stateObj = entityId ? this.hass!.states[entityId] : undefined; + const entityId = this._config.entity; + const featureContext = this._featureContext(entityId); return html` ({ + entity_id: entityId, + }) + ); + protected render() { if (!this.hass || !this._config) { return nothing; } - const entityId = this._config!.entity; - const stateObj = entityId ? this.hass!.states[entityId] : undefined; + const entityId = this._config.entity; + const featureContext = this._featureContext(entityId); return html` ({ + entity_id: entityId, + }) + ); + private _schema = memoizeOne( ( localize: LocalizeFunc, @@ -239,7 +244,8 @@ export class HuiTileCardEditor ); private _hasCompatibleFeatures = memoizeOne( - (stateObj: HassEntity) => getSupportedFeaturesType(stateObj).length > 0 + (context: LovelaceCardFeatureContext) => + getSupportedFeaturesType(this.hass!, context).length > 0 ); protected render() { @@ -248,7 +254,6 @@ export class HuiTileCardEditor } const entityId = this._config!.entity; - const stateObj = entityId ? this.hass!.states[entityId] : undefined; const schema = this._schema( this.hass.localize, @@ -271,8 +276,8 @@ export class HuiTileCardEditor data.features_position = "bottom"; } - const hasCompatibleFeatures = - (stateObj && this._hasCompatibleFeatures(stateObj)) || false; + const featureContext = this._featureContext(entityId); + const hasCompatibleFeatures = this._hasCompatibleFeatures(featureContext); return html` ): void { const index = ev.detail.subElementConfig.index; const config = this._config!.features![index!]; + const featureContext = this._featureContext(this._config!.entity); fireEvent(this, "edit-sub-element", { config: config, saveConfig: (newConfig) => this._updateFeature(index!, newConfig), - context: { - entity_id: this._config!.entity, - }, + context: featureContext, type: "feature", } as EditSubElementEvent< LovelaceCardFeatureConfig, diff --git a/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts b/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts index 065393e258..cbebee032b 100644 --- a/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts +++ b/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts @@ -13,7 +13,10 @@ import { supportsCoverOpenCloseCardFeature } from "../../../card-features/hui-co import { supportsLightBrightnessCardFeature } from "../../../card-features/hui-light-brightness-card-feature"; import { supportsLockCommandsCardFeature } from "../../../card-features/hui-lock-commands-card-feature"; import { supportsTargetTemperatureCardFeature } from "../../../card-features/hui-target-temperature-card-feature"; -import type { LovelaceCardFeatureConfig } from "../../../card-features/types"; +import type { + LovelaceCardFeatureConfig, + LovelaceCardFeatureContext, +} from "../../../card-features/types"; import type { TileCardConfig } from "../../../cards/types"; export const AREA_STRATEGY_GROUPS = [ @@ -206,6 +209,10 @@ export const computeAreaTileCardConfig = (entity: string): LovelaceCardConfig => { const stateObj = hass.states[entity]; + const context: LovelaceCardFeatureContext = { + entity_id: entity, + }; + const additionalCardConfig: Partial = {}; const domain = computeDomain(entity); @@ -225,23 +232,23 @@ export const computeAreaTileCardConfig = let feature: LovelaceCardFeatureConfig | undefined; if (includeFeature) { - if (supportsLightBrightnessCardFeature(stateObj)) { + if (supportsLightBrightnessCardFeature(hass, context)) { feature = { type: "light-brightness", }; - } else if (supportsCoverOpenCloseCardFeature(stateObj)) { + } else if (supportsCoverOpenCloseCardFeature(hass, context)) { feature = { type: "cover-open-close", }; - } else if (supportsTargetTemperatureCardFeature(stateObj)) { + } else if (supportsTargetTemperatureCardFeature(hass, context)) { feature = { type: "target-temperature", }; - } else if (supportsAlarmModesCardFeature(stateObj)) { + } else if (supportsAlarmModesCardFeature(hass, context)) { feature = { type: "alarm-modes", }; - } else if (supportsLockCommandsCardFeature(stateObj)) { + } else if (supportsLockCommandsCardFeature(hass, context)) { feature = { type: "lock-commands", }; diff --git a/src/panels/lovelace/types.ts b/src/panels/lovelace/types.ts index c3810265d9..8da041bb27 100644 --- a/src/panels/lovelace/types.ts +++ b/src/panels/lovelace/types.ts @@ -8,13 +8,16 @@ import type { LovelaceRawConfig, } from "../../data/lovelace/config/types"; import type { FrontendLocaleData } from "../../data/translation"; +import type { ShowToastParams } from "../../managers/notification-manager"; import type { Constructor, HomeAssistant } from "../../types"; +import type { + LovelaceCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./card-features/types"; +import type { LovelaceElement, LovelaceElementConfig } from "./elements/types"; import type { LovelaceRow, LovelaceRowConfig } from "./entity-rows/types"; import type { LovelaceHeaderFooterConfig } from "./header-footer/types"; -import type { LovelaceCardFeatureConfig } from "./card-features/types"; -import type { LovelaceElement, LovelaceElementConfig } from "./elements/types"; import type { LovelaceHeadingBadgeConfig } from "./heading-badges/types"; -import type { ShowToastParams } from "../../managers/notification-manager"; declare global { interface HASSDomEvents { @@ -169,7 +172,9 @@ export interface LovelaceGenericElementEditor extends HTMLElement { export interface LovelaceCardFeature extends HTMLElement { hass?: HomeAssistant; + /** @deprecated Use `context` instead */ stateObj?: HassEntity; + context?: LovelaceCardFeatureContext; setConfig(config: LovelaceCardFeatureConfig); color?: string; } @@ -178,7 +183,7 @@ export interface LovelaceCardFeatureConstructor extends Constructor { getStubConfig?: ( hass: HomeAssistant, - stateObj?: HassEntity + context?: LovelaceCardFeatureContext ) => LovelaceCardFeatureConfig; getConfigElement?: () => LovelaceCardFeatureEditor; getConfigForm?: () => {