From 1e010a196a0d03d9d1485fc9ec6a931f2c13322f Mon Sep 17 00:00:00 2001 From: victorigualada Date: Mon, 13 Oct 2025 11:21:58 +0200 Subject: [PATCH 1/7] show first used automation devices in the device pickers --- src/components/device/ha-device-picker.ts | 50 +++++- src/data/automation_editor_context.ts | 160 ++++++++++++++++++ .../config/automation/ha-automation-editor.ts | 17 +- 3 files changed, 221 insertions(+), 6 deletions(-) create mode 100644 src/data/automation_editor_context.ts diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index 4182ee106aa1..361a5a2e0316 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -3,6 +3,7 @@ import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; import { LitElement, html } from "lit"; import { customElement, property, query, state } from "lit/decorators"; +import { consume } from "@lit-labs/context"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../common/dom/fire_event"; import { computeDomain } from "../../common/entity/compute_domain"; @@ -22,6 +23,10 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types"; import "../ha-combo-box"; import type { HaComboBox } from "../ha-combo-box"; import "../ha-list-item"; +import { + automationEditorContext, + type AutomationLocalContext, +} from "../../data/automation_editor_context"; interface Device { name: string; @@ -101,6 +106,14 @@ export class HaDevicePicker extends LitElement { private _init = false; + @state() + @consume({ context: automationEditorContext, subscribe: true }) + private _autoCtx?: AutomationLocalContext; + + public get usedDevices(): string[] | undefined { + return this._autoCtx?.used?.devices; + } + private _getDevices = memoizeOne( ( devices: DeviceRegistryEntry[], @@ -111,7 +124,8 @@ export class HaDevicePicker extends LitElement { includeDeviceClasses: this["includeDeviceClasses"], deviceFilter: this["deviceFilter"], entityFilter: this["entityFilter"], - excludeDevices: this["excludeDevices"] + excludeDevices: this["excludeDevices"], + usedDevices: this["usedDevices"] ): ScorableDevice[] => { if (!devices.length) { return [ @@ -242,11 +256,24 @@ export class HaDevicePicker extends LitElement { }, ]; } - if (outputDevices.length === 1) { + // Reorder so used devices appear on top + const locale = this.hass.locale.language; + if (outputDevices.length <= 1) { return outputDevices; } + if (usedDevices?.length) { + const usedSet = new Set(usedDevices); + const used: ScorableDevice[] = []; + const rest: ScorableDevice[] = []; + for (const dev of outputDevices) { + (usedSet.has(dev.id) ? used : rest).push(dev); + } + used.sort((a, b) => stringCompare(a.name || "", b.name || "", locale)); + rest.sort((a, b) => stringCompare(a.name || "", b.name || "", locale)); + return [...used, ...rest]; + } return outputDevices.sort((a, b) => - stringCompare(a.name || "", b.name || "", this.hass.locale.language) + stringCompare(a.name || "", b.name || "", locale) ); } ); @@ -276,7 +303,8 @@ export class HaDevicePicker extends LitElement { this.includeDeviceClasses, this.deviceFilter, this.entityFilter, - this.excludeDevices + this.excludeDevices, + this.usedDevices ); this.comboBox.items = devices; this.comboBox.filteredItems = devices; @@ -312,9 +340,21 @@ export class HaDevicePicker extends LitElement { private _filterChanged(ev: CustomEvent): void { const target = ev.target as HaComboBox; const filterString = ev.detail.value.toLowerCase(); - target.filteredItems = filterString.length + const base = filterString.length ? fuzzyFilterSort(filterString, target.items || []) : target.items; + const usedDevices = this.usedDevices; + if (usedDevices?.length && base) { + const usedSet = new Set(usedDevices); + const used: ScorableDevice[] = []; + const rest: ScorableDevice[] = []; + for (const dev of base as ScorableDevice[]) { + (usedSet.has(dev.id) ? used : rest).push(dev); + } + target.filteredItems = [...used, ...rest]; + } else { + target.filteredItems = base as any; + } } private _deviceChanged(ev: ValueChangedEvent) { diff --git a/src/data/automation_editor_context.ts b/src/data/automation_editor_context.ts new file mode 100644 index 000000000000..fcae11a44c86 --- /dev/null +++ b/src/data/automation_editor_context.ts @@ -0,0 +1,160 @@ +import { createContext } from "@lit-labs/context"; +import type { HomeAssistant } from "../types"; +import type { AutomationConfig } from "./automation"; +import { ensureArray } from "../common/array/ensure-array"; + +export type AutomationSection = "triggers" | "conditions" | "actions"; + +export type EntityId = string; +export type DeviceId = string; +export type AreaId = string; +export type Domain = string; + +export interface AutomationLocalContext { + meta: { + automationId?: string; + }; + + used: { + entities: EntityId[]; + devices: DeviceId[]; + areas: AreaId[]; + domains: Domain[]; + }; + + freq: { + entities: Record; + devices: Record; + areas: Record; + domains: Record; + bySection?: Partial< + Record< + AutomationSection, + { + entities: Record; + devices: Record; + areas: Record; + domains: Record; + } + > + >; + }; + + maps: { + entityArea: Record; + entityDevice: Record; + deviceArea: Record; + deviceDomains: Record; + }; + + weights?: { + used: number; + sameDomain: number; + sameArea: number; + }; + + hints?: { + lastEditedPath?: string[] | number[]; + labels?: string[]; + services?: string[]; + }; +} + +/** + * Context key for the automation editor locality context. + */ +export const automationEditorContext = createContext< + AutomationLocalContext | undefined +>("automationEditorContext"); + +const addIds = ( + ids: unknown, + set: Set, + freq: Record +) => { + if (ids === undefined || ids === null) return; + const list = ensureArray(ids) as unknown[]; + for (const item of list) { + if (typeof item === "string" && item) { + set.add(item); + freq[item] = (freq[item] || 0) + 1; + } + } +}; + +export const buildAutomationLocalContext = ( + config: AutomationConfig | undefined, + _hass: HomeAssistant, + automationId?: string +): AutomationLocalContext | undefined => { + if (!config) return undefined; + + const devicesSet = new Set(); + const devicesFreq: Record = {}; + + const scan = (val: any) => { + if (val === null || val === undefined) return; + if (Array.isArray(val)) { + for (const item of val) scan(item); + return; + } + if (typeof val !== "object") return; + + if ("device_id" in val) { + addIds((val as any).device_id, devicesSet, devicesFreq); + } + if ("target" in val && val.target && typeof val.target === "object") { + if ("device_id" in (val.target as any)) { + addIds((val.target as any).device_id, devicesSet, devicesFreq); + } + } + + for (const key of Object.keys(val)) { + const child = (val as any)[key]; + if (child && typeof child === "object") { + scan(child); + } + } + }; + + scan(config.triggers); + scan(config.conditions); + scan(config.actions); + + // Sort used devices by frequency desc, then by id for stability + const usedDevices = Array.from(devicesSet).sort((a, b) => { + const fa = devicesFreq[a] || 0; + const fb = devicesFreq[b] || 0; + if (fb !== fa) return fb - fa; + return a.localeCompare(b); + }); + + return { + meta: { + automationId, + }, + used: { + entities: [], + devices: usedDevices, + areas: [], + domains: [], + }, + freq: { + entities: {}, + devices: devicesFreq, + areas: {}, + domains: {}, + }, + maps: { + entityArea: {}, + entityDevice: {}, + deviceArea: Object.fromEntries( + Object.entries(_hass.devices || {}).map(([id, dev]) => [ + id, + dev.area_id || null, + ]) + ), + deviceDomains: {}, + }, + }; +}; diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index 77ab99ab88e0..dd23b6d4e106 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -21,8 +21,8 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { property, state } from "lit/decorators"; +import { ContextProvider, consume } from "@lit-labs/context"; import { classMap } from "lit/directives/class-map"; -import { consume } from "@lit-labs/context"; import { fireEvent } from "../../../common/dom/fire_event"; import { navigate } from "../../../common/navigate"; import { computeRTL } from "../../../common/util/compute_rtl"; @@ -78,6 +78,10 @@ import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin"; import { fullEntitiesContext } from "../../../data/context"; +import { + automationEditorContext, + buildAutomationLocalContext, +} from "../../../data/automation_editor_context"; import { transform } from "../../../common/decorators/transform"; declare global { @@ -162,6 +166,11 @@ export class HaAutomationEditor extends PreventUnsavedMixin( value: PromiseLike | EntityRegistryEntry ) => void; + private _automationContextProvider = new ContextProvider(this, { + context: automationEditorContext, + initialValue: undefined, + }); + protected willUpdate(changedProps) { super.willUpdate(changedProps); @@ -571,6 +580,12 @@ export class HaAutomationEditor extends PreventUnsavedMixin( Object.values(this._configSubscriptions).forEach((sub) => sub(this._config) ); + const ctx = buildAutomationLocalContext( + this._config, + this.hass, + this.automationId ?? this._entityId ?? undefined + ); + this._automationContextProvider.setValue(ctx); } } From 43812080ac1c73324cb46a1017f48edd6348c23d Mon Sep 17 00:00:00 2001 From: victorigualada Date: Mon, 13 Oct 2025 12:14:18 +0200 Subject: [PATCH 2/7] move context to automation sections and pass down value --- src/components/device/ha-device-picker.ts | 28 ++++++------------- .../types/ha-automation-action-device_id.ts | 9 ++++++ .../types/ha-automation-condition-device.ts | 9 ++++++ .../types/ha-automation-trigger-device.ts | 9 ++++++ 4 files changed, 36 insertions(+), 19 deletions(-) diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index 361a5a2e0316..a7ed3a549f80 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -3,7 +3,6 @@ import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; import { LitElement, html } from "lit"; import { customElement, property, query, state } from "lit/decorators"; -import { consume } from "@lit-labs/context"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../common/dom/fire_event"; import { computeDomain } from "../../common/entity/compute_domain"; @@ -23,10 +22,6 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types"; import "../ha-combo-box"; import type { HaComboBox } from "../ha-combo-box"; import "../ha-list-item"; -import { - automationEditorContext, - type AutomationLocalContext, -} from "../../data/automation_editor_context"; interface Device { name: string; @@ -106,13 +101,8 @@ export class HaDevicePicker extends LitElement { private _init = false; - @state() - @consume({ context: automationEditorContext, subscribe: true }) - private _autoCtx?: AutomationLocalContext; - - public get usedDevices(): string[] | undefined { - return this._autoCtx?.used?.devices; - } + @property({ attribute: false, type: Array }) + public suggestedDevices?: string[]; private _getDevices = memoizeOne( ( @@ -125,7 +115,7 @@ export class HaDevicePicker extends LitElement { deviceFilter: this["deviceFilter"], entityFilter: this["entityFilter"], excludeDevices: this["excludeDevices"], - usedDevices: this["usedDevices"] + suggestedDevices: this["suggestedDevices"] ): ScorableDevice[] => { if (!devices.length) { return [ @@ -261,8 +251,8 @@ export class HaDevicePicker extends LitElement { if (outputDevices.length <= 1) { return outputDevices; } - if (usedDevices?.length) { - const usedSet = new Set(usedDevices); + if (suggestedDevices?.length) { + const usedSet = new Set(suggestedDevices); const used: ScorableDevice[] = []; const rest: ScorableDevice[] = []; for (const dev of outputDevices) { @@ -304,7 +294,7 @@ export class HaDevicePicker extends LitElement { this.deviceFilter, this.entityFilter, this.excludeDevices, - this.usedDevices + this.suggestedDevices ); this.comboBox.items = devices; this.comboBox.filteredItems = devices; @@ -343,9 +333,9 @@ export class HaDevicePicker extends LitElement { const base = filterString.length ? fuzzyFilterSort(filterString, target.items || []) : target.items; - const usedDevices = this.usedDevices; - if (usedDevices?.length && base) { - const usedSet = new Set(usedDevices); + const suggestedDevices = this.suggestedDevices; + if (suggestedDevices?.length && base) { + const usedSet = new Set(suggestedDevices); const used: ScorableDevice[] = []; const rest: ScorableDevice[] = []; for (const dev of base as ScorableDevice[]) { 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 e5f4f3d9d4b5..7715965dcdd7 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 @@ -8,6 +8,10 @@ import "../../../../../components/device/ha-device-action-picker"; import "../../../../../components/device/ha-device-picker"; import "../../../../../components/ha-form/ha-form"; import { fullEntitiesContext } from "../../../../../data/context"; +import { + automationEditorContext, + type AutomationLocalContext, +} from "../../../../../data/automation_editor_context"; import type { DeviceAction, DeviceCapabilities, @@ -37,6 +41,10 @@ export class HaDeviceAction extends LitElement { @consume({ context: fullEntitiesContext, subscribe: true }) _entityReg!: EntityRegistryEntry[]; + @state() + @consume({ context: automationEditorContext, subscribe: true }) + private _automationCtx?: AutomationLocalContext; + private _origAction?: DeviceAction; public static get defaultConfig(): DeviceAction { @@ -87,6 +95,7 @@ export class HaDeviceAction extends LitElement { return html` Date: Mon, 13 Oct 2025 12:59:58 +0200 Subject: [PATCH 3/7] extract duplicated logic to helper function --- src/components/device/ha-device-picker.ts | 70 ++++++++++++++--------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index a7ed3a549f80..a109b631c47b 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -37,6 +37,28 @@ export type HaDevicePickerDeviceFilterFunc = ( export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean; +const reorderSuggestedFirstById = ( + items: T[] | undefined, + suggested?: string[] +): T[] => { + if (!items || !items.length) return []; + if (!suggested || !suggested.length) return items.slice(); + + const set = new Set(suggested); + const top: T[] = []; + const rest: T[] = []; + + for (const it of items) { + if (set.has(it.id)) { + top.push(it); + } else { + rest.push(it); + } + } + + return [...top, ...rest]; +}; + const rowRenderer: ComboBoxLitRenderer = (item) => html` ${item.name} @@ -246,25 +268,24 @@ export class HaDevicePicker extends LitElement { }, ]; } - // Reorder so used devices appear on top + const locale = this.hass.locale.language; - if (outputDevices.length <= 1) { + if (outputDevices.length === 1) { return outputDevices; } - if (suggestedDevices?.length) { - const usedSet = new Set(suggestedDevices); - const used: ScorableDevice[] = []; - const rest: ScorableDevice[] = []; - for (const dev of outputDevices) { - (usedSet.has(dev.id) ? used : rest).push(dev); - } - used.sort((a, b) => stringCompare(a.name || "", b.name || "", locale)); - rest.sort((a, b) => stringCompare(a.name || "", b.name || "", locale)); - return [...used, ...rest]; - } - return outputDevices.sort((a, b) => + + outputDevices.sort((a, b) => stringCompare(a.name || "", b.name || "", locale) ); + + if (suggestedDevices?.length) { + return reorderSuggestedFirstById( + outputDevices, + suggestedDevices + ); + } + + return outputDevices; } ); @@ -330,21 +351,14 @@ export class HaDevicePicker extends LitElement { private _filterChanged(ev: CustomEvent): void { const target = ev.target as HaComboBox; const filterString = ev.detail.value.toLowerCase(); + const source = (target.items || []) as ScorableDevice[]; const base = filterString.length - ? fuzzyFilterSort(filterString, target.items || []) - : target.items; - const suggestedDevices = this.suggestedDevices; - if (suggestedDevices?.length && base) { - const usedSet = new Set(suggestedDevices); - const used: ScorableDevice[] = []; - const rest: ScorableDevice[] = []; - for (const dev of base as ScorableDevice[]) { - (usedSet.has(dev.id) ? used : rest).push(dev); - } - target.filteredItems = [...used, ...rest]; - } else { - target.filteredItems = base as any; - } + ? fuzzyFilterSort(filterString, source) + : source; + target.filteredItems = reorderSuggestedFirstById( + base, + this.suggestedDevices + ); } private _deviceChanged(ev: ValueChangedEvent) { From 1c6b2d58b1f44ea96fada36193e8c5c57d0a7059 Mon Sep 17 00:00:00 2001 From: victorigualada Date: Mon, 13 Oct 2025 13:12:24 +0200 Subject: [PATCH 4/7] rename helper reorder function and replace assignement --- src/components/device/ha-device-picker.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index a109b631c47b..56cd6256b4f9 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -37,7 +37,7 @@ export type HaDevicePickerDeviceFilterFunc = ( export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean; -const reorderSuggestedFirstById = ( +const reorderWithSuggestedDevicesFirst = ( items: T[] | undefined, suggested?: string[] ): T[] => { @@ -269,17 +269,17 @@ export class HaDevicePicker extends LitElement { ]; } - const locale = this.hass.locale.language; if (outputDevices.length === 1) { return outputDevices; } + const locale = this.hass.locale.language; outputDevices.sort((a, b) => stringCompare(a.name || "", b.name || "", locale) ); if (suggestedDevices?.length) { - return reorderSuggestedFirstById( + return reorderWithSuggestedDevicesFirst( outputDevices, suggestedDevices ); @@ -355,7 +355,7 @@ export class HaDevicePicker extends LitElement { const base = filterString.length ? fuzzyFilterSort(filterString, source) : source; - target.filteredItems = reorderSuggestedFirstById( + target.filteredItems = reorderWithSuggestedDevicesFirst( base, this.suggestedDevices ); From c686917cb0becc45fca746b8c1bbcb777256ae3e Mon Sep 17 00:00:00 2001 From: victorigualada Date: Mon, 13 Oct 2025 14:56:43 +0200 Subject: [PATCH 5/7] update dependency --- src/data/automation_editor_context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/automation_editor_context.ts b/src/data/automation_editor_context.ts index fcae11a44c86..ab705dc73ed5 100644 --- a/src/data/automation_editor_context.ts +++ b/src/data/automation_editor_context.ts @@ -1,4 +1,4 @@ -import { createContext } from "@lit-labs/context"; +import { createContext } from "@lit/context"; import type { HomeAssistant } from "../types"; import type { AutomationConfig } from "./automation"; import { ensureArray } from "../common/array/ensure-array"; From d864a474b1ffb072a202bc47d17bc0f2382f9cb2 Mon Sep 17 00:00:00 2001 From: victorigualada Date: Mon, 13 Oct 2025 15:19:31 +0200 Subject: [PATCH 6/7] remove unneeded search and reorder functions --- src/components/device/ha-device-picker.ts | 32 ----------------------- src/data/automation_editor_context.ts | 2 +- 2 files changed, 1 insertion(+), 33 deletions(-) diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index 7fbc93bfda1a..0f3ab03f5798 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -30,28 +30,6 @@ export type HaDevicePickerDeviceFilterFunc = ( export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean; -// Helper: move suggested ids to front, preserving within-group order -const reorderSuggestedFirstById = ( - items: T[] | undefined, - suggested?: string[] -): T[] => { - if (!items || !items.length) return []; - if (!suggested || !suggested.length) return items.slice(); - - const set = new Set(suggested); - const top: T[] = []; - const rest: T[] = []; - - for (const it of items) { - if (set.has(it.id)) { - top.push(it); - } else { - rest.push(it); - } - } - return [...top, ...rest]; -}; - interface DevicePickerItem extends PickerComboBoxItem { domain?: string; domain_name?: string; @@ -404,7 +382,6 @@ export class HaDevicePicker extends LitElement { .getItems=${this._getItems} .hideClearIcon=${this.hideClearIcon} .valueRenderer=${valueRenderer} - .searchFn=${this._searchFn} @value-changed=${this._valueChanged} > @@ -422,15 +399,6 @@ export class HaDevicePicker extends LitElement { this.value = value; fireEvent(this, "value-changed", { value }); } - - // Reorder filtered results to place suggestions first while preserving - // the fuzzy ranking within groups. - private _searchFn: ( - search: string, - filteredItems: PickerComboBoxItem[], - allItems: PickerComboBoxItem[] - ) => PickerComboBoxItem[] = (_s, filtered, _all) => - reorderSuggestedFirstById(filtered, this.suggestedDevices); } declare global { diff --git a/src/data/automation_editor_context.ts b/src/data/automation_editor_context.ts index fcae11a44c86..ab705dc73ed5 100644 --- a/src/data/automation_editor_context.ts +++ b/src/data/automation_editor_context.ts @@ -1,4 +1,4 @@ -import { createContext } from "@lit-labs/context"; +import { createContext } from "@lit/context"; import type { HomeAssistant } from "../types"; import type { AutomationConfig } from "./automation"; import { ensureArray } from "../common/array/ensure-array"; From 394ea460d64b08c951175b1e1c08ba069b90e520 Mon Sep 17 00:00:00 2001 From: victorigualada Date: Mon, 13 Oct 2025 17:03:26 +0200 Subject: [PATCH 7/7] ensure we don't re-render if devices didn't change in the automation --- src/data/automation_editor_context.ts | 61 ++++++------------- .../config/automation/ha-automation-editor.ts | 7 ++- 2 files changed, 25 insertions(+), 43 deletions(-) diff --git a/src/data/automation_editor_context.ts b/src/data/automation_editor_context.ts index ab705dc73ed5..c1d56818c266 100644 --- a/src/data/automation_editor_context.ts +++ b/src/data/automation_editor_context.ts @@ -13,6 +13,7 @@ export type Domain = string; export interface AutomationLocalContext { meta: { automationId?: string; + signature: string; }; used: { @@ -22,24 +23,6 @@ export interface AutomationLocalContext { domains: Domain[]; }; - freq: { - entities: Record; - devices: Record; - areas: Record; - domains: Record; - bySection?: Partial< - Record< - AutomationSection, - { - entities: Record; - devices: Record; - areas: Record; - domains: Record; - } - > - >; - }; - maps: { entityArea: Record; entityDevice: Record; @@ -67,30 +50,31 @@ export const automationEditorContext = createContext< AutomationLocalContext | undefined >("automationEditorContext"); -const addIds = ( - ids: unknown, - set: Set, - freq: Record -) => { +const addIds = (ids: unknown, set: Set) => { if (ids === undefined || ids === null) return; const list = ensureArray(ids) as unknown[]; for (const item of list) { if (typeof item === "string" && item) { set.add(item); - freq[item] = (freq[item] || 0) + 1; } } }; +const getContextSignature = (devices: Set): string => + Array.from(devices.values()) + .map((d) => d.slice(0, 6)) + .sort() + .join(); + export const buildAutomationLocalContext = ( config: AutomationConfig | undefined, _hass: HomeAssistant, - automationId?: string + automationId?: string, + previous?: AutomationLocalContext ): AutomationLocalContext | undefined => { if (!config) return undefined; const devicesSet = new Set(); - const devicesFreq: Record = {}; const scan = (val: any) => { if (val === null || val === undefined) return; @@ -101,11 +85,11 @@ export const buildAutomationLocalContext = ( if (typeof val !== "object") return; if ("device_id" in val) { - addIds((val as any).device_id, devicesSet, devicesFreq); + addIds((val as any).device_id, devicesSet); } if ("target" in val && val.target && typeof val.target === "object") { if ("device_id" in (val.target as any)) { - addIds((val.target as any).device_id, devicesSet, devicesFreq); + addIds((val.target as any).device_id, devicesSet); } } @@ -121,30 +105,23 @@ export const buildAutomationLocalContext = ( scan(config.conditions); scan(config.actions); - // Sort used devices by frequency desc, then by id for stability - const usedDevices = Array.from(devicesSet).sort((a, b) => { - const fa = devicesFreq[a] || 0; - const fb = devicesFreq[b] || 0; - if (fb !== fa) return fb - fa; - return a.localeCompare(b); - }); + // Avoid re-rendering if nothing changed + const signature = getContextSignature(devicesSet); + if (previous && previous.meta.signature === signature) { + return previous; + } return { meta: { automationId, + signature, }, used: { entities: [], - devices: usedDevices, + devices: Array.from(devicesSet), areas: [], domains: [], }, - freq: { - entities: {}, - devices: devicesFreq, - areas: {}, - domains: {}, - }, maps: { entityArea: {}, entityDevice: {}, diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index 0d306deab701..7af76b8e1fbb 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -93,6 +93,7 @@ import { automationEditorContext, buildAutomationLocalContext, } from "../../../data/automation_editor_context"; +import type { AutomationLocalContext } from "../../../data/automation_editor_context"; declare global { interface HTMLElementTagNameMap { @@ -194,6 +195,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin( initialValue: undefined, }); + private _automationContextValue?: AutomationLocalContext; + protected willUpdate(changedProps) { super.willUpdate(changedProps); @@ -728,9 +731,11 @@ export class HaAutomationEditor extends PreventUnsavedMixin( const ctx = buildAutomationLocalContext( this._config, this.hass, - this.automationId ?? this._entityId ?? undefined + this.automationId ?? this._entityId ?? undefined, + this._automationContextValue ); this._automationContextProvider.setValue(ctx); + this._automationContextValue = ctx; } }