From 2dfe5f50a6cedf7ac7f0b52828f7edff1d5e7cdc Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Wed, 25 Jun 2025 06:20:53 -0700 Subject: [PATCH] Support templates in action target (#25656) --- src/components/ha-service-control.ts | 43 ++++++++++++++++--- src/data/script.ts | 16 ++++++- .../types/ha-automation-action-service.ts | 2 +- .../action/developer-tools-action.ts | 2 +- 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/src/components/ha-service-control.ts b/src/components/ha-service-control.ts index a31382ac2f..898959277d 100644 --- a/src/components/ha-service-control.ts +++ b/src/components/ha-service-control.ts @@ -276,6 +276,16 @@ export class HaServiceControl extends LitElement { private _getTargetedEntities = memoizeOne((target, value) => { const targetSelector = target ? { target } : { target: {} }; + if ( + hasTemplate(value?.target) || + hasTemplate(value?.data?.entity_id) || + hasTemplate(value?.data?.device_id) || + hasTemplate(value?.data?.area_id) || + hasTemplate(value?.data?.floor_id) || + hasTemplate(value?.data?.label_id) + ) { + return null; + } const targetEntities = ensureArray( value?.target?.entity_id || value?.data?.entity_id @@ -349,8 +359,11 @@ export class HaServiceControl extends LitElement { private _filterField( filter: ExtHassService["fields"][number]["filter"], - targetEntities: string[] + targetEntities: string[] | null ) { + if (targetEntities === null) { + return true; // Target is a template, show all fields + } if (!targetEntities.length) { return false; } @@ -386,8 +399,21 @@ export class HaServiceControl extends LitElement { } private _targetSelector = memoizeOne( - (targetSelector: TargetSelector | null | undefined) => - targetSelector ? { target: { ...targetSelector } } : { target: {} } + (targetSelector: TargetSelector | null | undefined, value) => { + if (!value || (typeof value === "object" && !Object.keys(value).length)) { + delete this._stickySelector.target; + } else if (hasTemplate(value)) { + if (typeof value === "string") { + this._stickySelector.target = { template: null }; + } else { + this._stickySelector.target = { object: null }; + } + } + return ( + this._stickySelector.target ?? + (targetSelector ? { target: { ...targetSelector } } : { target: {} }) + ); + } ); protected render() { @@ -482,7 +508,8 @@ export class HaServiceControl extends LitElement { > @@ -588,7 +615,7 @@ export class HaServiceControl extends LitElement { hasOptional: boolean, domain: string | undefined, serviceName: string | undefined, - targetEntities: string[] + targetEntities: string[] | null ) => { if ( dataField.filter && @@ -822,6 +849,10 @@ export class HaServiceControl extends LitElement { private _targetChanged(ev: CustomEvent) { ev.stopPropagation(); + if (ev.detail.isValid === false) { + // Don't clear an object selector that returns invalid YAML + return; + } const newValue = ev.detail.value; if (this._value?.target === newValue) { return; diff --git a/src/data/script.ts b/src/data/script.ts index 6128014dfc..c7ba8d6c79 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -14,6 +14,7 @@ import { literal, is, boolean, + refine, } from "superstruct"; import { arrayLiteralIncludes } from "../common/array/literal-includes"; import { navigate } from "../common/navigate"; @@ -49,13 +50,18 @@ export const targetStruct = object({ label_id: optional(union([string(), array(string())])), }); -export const serviceActionStruct: Describe = assign( +export const serviceActionStruct: Describe = assign( baseActionStruct, object({ action: optional(string()), service_template: optional(string()), entity_id: optional(string()), - target: optional(targetStruct), + target: optional( + union([ + targetStruct, + refine(string(), "has_template", (val) => hasTemplate(val)), + ]) + ), data: optional(object()), response_variable: optional(string()), metadata: optional(object()), @@ -132,6 +138,12 @@ export interface ServiceAction extends BaseAction { metadata?: Record; } +type ServiceActionWithTemplate = ServiceAction & { + target?: HassServiceTarget | string; +}; + +export type { ServiceActionWithTemplate }; + export interface DeviceAction extends BaseAction { type: string; device_id: string; diff --git a/src/panels/config/automation/action/types/ha-automation-action-service.ts b/src/panels/config/automation/action/types/ha-automation-action-service.ts index fa431c2f50..20f2da1c5a 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-service.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-service.ts @@ -42,7 +42,7 @@ export class HaServiceAction extends LitElement implements ActionElement { if ( this.action && Object.entries(this.action).some( - ([key, val]) => key !== "data" && hasTemplate(val) + ([key, val]) => !["data", "target"].includes(key) && hasTemplate(val) ) ) { fireEvent( diff --git a/src/panels/developer-tools/action/developer-tools-action.ts b/src/panels/developer-tools/action/developer-tools-action.ts index d964555e27..d28f89c714 100644 --- a/src/panels/developer-tools/action/developer-tools-action.ts +++ b/src/panels/developer-tools/action/developer-tools-action.ts @@ -535,7 +535,7 @@ class HaPanelDevAction extends LitElement { if ( this._serviceData && Object.entries(this._serviceData).some( - ([key, val]) => key !== "data" && hasTemplate(val) + ([key, val]) => !["data", "target"].includes(key) && hasTemplate(val) ) ) { this._yamlMode = true;