Support templates in action target (#25656)

This commit is contained in:
karwosts 2025-06-25 06:20:53 -07:00 committed by GitHub
parent 174d54396f
commit 2dfe5f50a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 53 additions and 10 deletions

View File

@ -276,6 +276,16 @@ export class HaServiceControl extends LitElement {
private _getTargetedEntities = memoizeOne((target, value) => { private _getTargetedEntities = memoizeOne((target, value) => {
const targetSelector = target ? { target } : { target: {} }; 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 = const targetEntities =
ensureArray( ensureArray(
value?.target?.entity_id || value?.data?.entity_id value?.target?.entity_id || value?.data?.entity_id
@ -349,8 +359,11 @@ export class HaServiceControl extends LitElement {
private _filterField( private _filterField(
filter: ExtHassService["fields"][number]["filter"], 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) { if (!targetEntities.length) {
return false; return false;
} }
@ -386,8 +399,21 @@ export class HaServiceControl extends LitElement {
} }
private _targetSelector = memoizeOne( private _targetSelector = memoizeOne(
(targetSelector: TargetSelector | null | undefined) => (targetSelector: TargetSelector | null | undefined, value) => {
targetSelector ? { target: { ...targetSelector } } : { target: {} } 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() { protected render() {
@ -482,7 +508,8 @@ export class HaServiceControl extends LitElement {
><ha-selector ><ha-selector
.hass=${this.hass} .hass=${this.hass}
.selector=${this._targetSelector( .selector=${this._targetSelector(
serviceData.target as TargetSelector serviceData.target as TargetSelector,
this._value?.target
)} )}
.disabled=${this.disabled} .disabled=${this.disabled}
@value-changed=${this._targetChanged} @value-changed=${this._targetChanged}
@ -575,7 +602,7 @@ export class HaServiceControl extends LitElement {
private _hasFilteredFields( private _hasFilteredFields(
dataFields: ExtHassService["fields"], dataFields: ExtHassService["fields"],
targetEntities: string[] targetEntities: string[] | null
) { ) {
return dataFields.some( return dataFields.some(
(dataField) => (dataField) =>
@ -588,7 +615,7 @@ export class HaServiceControl extends LitElement {
hasOptional: boolean, hasOptional: boolean,
domain: string | undefined, domain: string | undefined,
serviceName: string | undefined, serviceName: string | undefined,
targetEntities: string[] targetEntities: string[] | null
) => { ) => {
if ( if (
dataField.filter && dataField.filter &&
@ -822,6 +849,10 @@ export class HaServiceControl extends LitElement {
private _targetChanged(ev: CustomEvent) { private _targetChanged(ev: CustomEvent) {
ev.stopPropagation(); ev.stopPropagation();
if (ev.detail.isValid === false) {
// Don't clear an object selector that returns invalid YAML
return;
}
const newValue = ev.detail.value; const newValue = ev.detail.value;
if (this._value?.target === newValue) { if (this._value?.target === newValue) {
return; return;

View File

@ -14,6 +14,7 @@ import {
literal, literal,
is, is,
boolean, boolean,
refine,
} from "superstruct"; } from "superstruct";
import { arrayLiteralIncludes } from "../common/array/literal-includes"; import { arrayLiteralIncludes } from "../common/array/literal-includes";
import { navigate } from "../common/navigate"; import { navigate } from "../common/navigate";
@ -49,13 +50,18 @@ export const targetStruct = object({
label_id: optional(union([string(), array(string())])), label_id: optional(union([string(), array(string())])),
}); });
export const serviceActionStruct: Describe<ServiceAction> = assign( export const serviceActionStruct: Describe<ServiceActionWithTemplate> = assign(
baseActionStruct, baseActionStruct,
object({ object({
action: optional(string()), action: optional(string()),
service_template: optional(string()), service_template: optional(string()),
entity_id: optional(string()), entity_id: optional(string()),
target: optional(targetStruct), target: optional(
union([
targetStruct,
refine(string(), "has_template", (val) => hasTemplate(val)),
])
),
data: optional(object()), data: optional(object()),
response_variable: optional(string()), response_variable: optional(string()),
metadata: optional(object()), metadata: optional(object()),
@ -132,6 +138,12 @@ export interface ServiceAction extends BaseAction {
metadata?: Record<string, unknown>; metadata?: Record<string, unknown>;
} }
type ServiceActionWithTemplate = ServiceAction & {
target?: HassServiceTarget | string;
};
export type { ServiceActionWithTemplate };
export interface DeviceAction extends BaseAction { export interface DeviceAction extends BaseAction {
type: string; type: string;
device_id: string; device_id: string;

View File

@ -42,7 +42,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
if ( if (
this.action && this.action &&
Object.entries(this.action).some( Object.entries(this.action).some(
([key, val]) => key !== "data" && hasTemplate(val) ([key, val]) => !["data", "target"].includes(key) && hasTemplate(val)
) )
) { ) {
fireEvent( fireEvent(

View File

@ -535,7 +535,7 @@ class HaPanelDevAction extends LitElement {
if ( if (
this._serviceData && this._serviceData &&
Object.entries(this._serviceData).some( Object.entries(this._serviceData).some(
([key, val]) => key !== "data" && hasTemplate(val) ([key, val]) => !["data", "target"].includes(key) && hasTemplate(val)
) )
) { ) {
this._yamlMode = true; this._yamlMode = true;