diff --git a/src/components/ha-service-control.ts b/src/components/ha-service-control.ts index cbf71987e0..20cf293874 100644 --- a/src/components/ha-service-control.ts +++ b/src/components/ha-service-control.ts @@ -77,7 +77,7 @@ export class HaServiceControl extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public value?: { - service: string; + action: string; target?: HassServiceTarget; data?: Record; }; @@ -112,23 +112,23 @@ export class HaServiceControl extends LitElement { | undefined | this["value"]; - if (oldValue?.service !== this.value?.service) { + if (oldValue?.action !== this.value?.action) { this._checkedKeys = new Set(); } const serviceData = this._getServiceInfo( - this.value?.service, + this.value?.action, this.hass.services ); // Fetch the manifest if we have a service selected and the service domain changed. // If no service is selected, clear the manifest. - if (this.value?.service) { + if (this.value?.action) { if ( - !oldValue?.service || - computeDomain(this.value.service) !== computeDomain(oldValue.service) + !oldValue?.action || + computeDomain(this.value.action) !== computeDomain(oldValue.action) ) { - this._fetchManifest(computeDomain(this.value?.service)); + this._fetchManifest(computeDomain(this.value?.action)); } } else { this._manifest = undefined; @@ -168,7 +168,7 @@ export class HaServiceControl extends LitElement { this._value = this.value; } - if (oldValue?.service !== this.value?.service) { + if (oldValue?.action !== this.value?.action) { let updatedDefaultValue = false; if (this._value && serviceData) { const loadDefaults = this.value && !("data" in this.value); @@ -367,7 +367,7 @@ export class HaServiceControl extends LitElement { protected render() { const serviceData = this._getServiceInfo( - this._value?.service, + this._value?.action, this.hass.services ); @@ -392,11 +392,11 @@ export class HaServiceControl extends LitElement { this._value ); - const domain = this._value?.service - ? computeDomain(this._value.service) + const domain = this._value?.action + ? computeDomain(this._value.action) : undefined; - const serviceName = this._value?.service - ? computeObjectId(this._value.service) + const serviceName = this._value?.action + ? computeObjectId(this._value.action) : undefined; const description = @@ -410,7 +410,7 @@ export class HaServiceControl extends LitElement { ? nothing : html``} @@ -596,11 +596,11 @@ export class HaServiceControl extends LitElement { }; private _localizeValueCallback = (key: string) => { - if (!this._value?.service) { + if (!this._value?.action) { return ""; } return this.hass.localize( - `component.${computeDomain(this._value.service)}.selector.${key}` + `component.${computeDomain(this._value.action)}.selector.${key}` ); }; @@ -612,7 +612,7 @@ export class HaServiceControl extends LitElement { if (checked) { this._checkedKeys.add(key); const field = this._getServiceInfo( - this._value?.service, + this._value?.action, this.hass.services )?.fields.find((_field) => _field.key === key); @@ -658,7 +658,7 @@ export class HaServiceControl extends LitElement { private _serviceChanged(ev: ValueChangedEvent) { ev.stopPropagation(); - if (ev.detail.value === this._value?.service) { + if (ev.detail.value === this._value?.action) { return; } diff --git a/src/components/trace/hat-script-graph.ts b/src/components/trace/hat-script-graph.ts index f9aaf73050..804261d6ff 100644 --- a/src/components/trace/hat-script-graph.ts +++ b/src/components/trace/hat-script-graph.ts @@ -424,7 +424,7 @@ export class HatScriptGraph extends LitElement { return html` tr.error)} tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"} > - ${node.service + ${node.action ? html`` : nothing} diff --git a/src/data/automation.ts b/src/data/automation.ts index ea20b874c8..59cf4bacfe 100644 --- a/src/data/automation.ts +++ b/src/data/automation.ts @@ -6,7 +6,7 @@ import { navigate } from "../common/navigate"; import { Context, HomeAssistant } from "../types"; import { BlueprintInput } from "./blueprint"; import { DeviceCondition, DeviceTrigger } from "./device_automation"; -import { Action, MODES } from "./script"; +import { Action, MODES, migrateAutomationAction } from "./script"; export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single"; export const AUTOMATION_DEFAULT_MAX = 10; @@ -28,7 +28,7 @@ export interface ManualAutomationConfig { description?: string; trigger: Trigger | Trigger[]; condition?: Condition | Condition[]; - action: Action | Action[]; + action?: Action | Action[]; mode?: (typeof MODES)[number]; max?: number; max_exceeded?: @@ -357,7 +357,7 @@ export const normalizeAutomationConfig = < >( config: T ): T => { - // Normalize data: ensure trigger, action and condition are lists + // Normalize data: ensure triggers, actions and conditions are lists // Happens when people copy paste their automations into the config for (const key of ["trigger", "condition", "action"]) { const value = config[key]; @@ -365,6 +365,9 @@ export const normalizeAutomationConfig = < config[key] = [value]; } } + + config.action = migrateAutomationAction(config.action || []); + return config; }; diff --git a/src/data/script.ts b/src/data/script.ts index f70ca2d338..5bc5786c62 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -49,7 +49,7 @@ const targetStruct = object({ export const serviceActionStruct: Describe = assign( baseActionStruct, object({ - service: optional(string()), + action: optional(string()), service_template: optional(string()), entity_id: optional(string()), target: optional(targetStruct), @@ -62,7 +62,7 @@ export const serviceActionStruct: Describe = assign( const playMediaActionStruct: Describe = assign( baseActionStruct, object({ - service: literal("media_player.play_media"), + action: literal("media_player.play_media"), target: optional(object({ entity_id: optional(string()) })), entity_id: optional(string()), data: object({ media_content_id: string(), media_content_type: string() }), @@ -73,7 +73,7 @@ const playMediaActionStruct: Describe = assign( const activateSceneActionStruct: Describe = assign( baseActionStruct, object({ - service: literal("scene.turn_on"), + action: literal("scene.turn_on"), target: optional(object({ entity_id: optional(string()) })), entity_id: optional(string()), metadata: object(), @@ -132,7 +132,7 @@ export interface EventAction extends BaseAction { } export interface ServiceAction extends BaseAction { - service?: string; + action?: string; service_template?: string; entity_id?: string; target?: HassServiceTarget; @@ -160,7 +160,7 @@ export interface DelayAction extends BaseAction { } export interface ServiceSceneAction extends BaseAction { - service: "scene.turn_on"; + action: "scene.turn_on"; target?: { entity_id?: string }; entity_id?: string; metadata: Record; @@ -191,7 +191,7 @@ export interface WaitForTriggerAction extends BaseAction { } export interface PlayMediaAction extends BaseAction { - service: "media_player.play_media"; + action: "media_player.play_media"; target?: { entity_id?: string }; entity_id?: string; data: { media_content_id: string; media_content_type: string }; @@ -404,7 +404,7 @@ export const getActionType = (action: Action): ActionType => { if ("set_conversation_response" in action) { return "set_conversation_response"; } - if ("service" in action) { + if ("action" in action) { if ("metadata" in action) { if (is(action, activateSceneActionStruct)) { return "activate_scene"; @@ -425,3 +425,60 @@ export const hasScriptFields = ( const fields = hass.services.script[computeObjectId(entityId)]?.fields; return fields !== undefined && Object.keys(fields).length > 0; }; + +export const migrateAutomationAction = ( + action: Action | Action[] +): Action | Action[] => { + if (Array.isArray(action)) { + return action.map(migrateAutomationAction) as Action[]; + } + + if ("service" in action) { + if (!("action" in action)) { + action.action = action.service; + } + delete action.service; + } + + if ("sequence" in action) { + for (const sequenceAction of (action as SequenceAction).sequence) { + migrateAutomationAction(sequenceAction); + } + } + + const actionType = getActionType(action); + + if (actionType === "parallel") { + const _action = action as ParallelAction; + migrateAutomationAction(_action.parallel); + } + + if (actionType === "choose") { + const _action = action as ChooseAction; + if (Array.isArray(_action.choose)) { + for (const choice of _action.choose) { + migrateAutomationAction(choice.sequence); + } + } else if (_action.choose) { + migrateAutomationAction(_action.choose.sequence); + } + if (_action.default) { + migrateAutomationAction(_action.default); + } + } + + if (actionType === "repeat") { + const _action = action as RepeatAction; + migrateAutomationAction(_action.repeat.sequence); + } + + if (actionType === "if") { + const _action = action as IfAction; + migrateAutomationAction(_action.then); + if (_action.else) { + migrateAutomationAction(_action.else); + } + } + + return action; +}; diff --git a/src/data/script_i18n.ts b/src/data/script_i18n.ts index 4d518bd57c..150afc4220 100644 --- a/src/data/script_i18n.ts +++ b/src/data/script_i18n.ts @@ -192,7 +192,7 @@ const tryDescribeAction = ( if ( config.service_template || - (config.service && isTemplate(config.service)) + (config.action && isTemplate(config.action)) ) { return hass.localize( targets.length @@ -204,8 +204,8 @@ const tryDescribeAction = ( ); } - if (config.service) { - const [domain, serviceName] = config.service.split(".", 2); + if (config.action) { + const [domain, serviceName] = config.action.split(".", 2); const service = hass.localize(`component.${domain}.services.${serviceName}.name`) || hass.services[domain][serviceName]?.name; @@ -217,7 +217,7 @@ const tryDescribeAction = ( : `${actionTranslationBaseKey}.service.description.service_name_no_targets`, { domain: domainToName(hass.localize, domain), - name: service || config.service, + name: service || config.action, targets: formatListWithAnds(hass.locale, targets), } ); @@ -230,7 +230,7 @@ const tryDescribeAction = ( { name: service ? `${domainToName(hass.localize, domain)}: ${service}` - : config.service, + : config.action, targets: formatListWithAnds(hass.locale, targets), } ); diff --git a/src/dialogs/more-info/controls/more-info-script.ts b/src/dialogs/more-info/controls/more-info-script.ts index c530f05cf5..83daa7dccf 100644 --- a/src/dialogs/more-info/controls/more-info-script.ts +++ b/src/dialogs/more-info/controls/more-info-script.ts @@ -148,7 +148,7 @@ class MoreInfoScript extends LitElement { const newState = this.stateObj; if (newState && (!oldState || oldState.entity_id !== newState.entity_id)) { - this._scriptData = { service: newState.entity_id, data: {} }; + this._scriptData = { action: newState.entity_id, data: {} }; } } diff --git a/src/panels/config/automation/action/ha-automation-action-row.ts b/src/panels/config/automation/action/ha-automation-action-row.ts index 0b15bc8acf..5da36548e4 100644 --- a/src/panels/config/automation/action/ha-automation-action-row.ts +++ b/src/panels/config/automation/action/ha-automation-action-row.ts @@ -87,8 +87,8 @@ export const getType = (action: Action | undefined) => { if (!action) { return undefined; } - if ("service" in action || "scene" in action) { - return getActionType(action) as "activate_scene" | "service" | "play_media"; + if ("action" in action || "scene" in action) { + return getActionType(action) as "activate_scene" | "action" | "play_media"; } if (["and", "or", "not"].some((key) => key in action)) { return "condition" as const; @@ -214,12 +214,12 @@ export default class HaAutomationActionRow extends LitElement {

${type === "service" && - "service" in this.action && - this.action.service + "action" in this.action && + this.action.action ? html`` : html`) { ev.stopPropagation(); + this._config = ev.detail.value; if (this._readOnly) { return; diff --git a/src/panels/developer-tools/action/developer-tools-action.ts b/src/panels/developer-tools/action/developer-tools-action.ts index 2bbb69d5bf..ccd8b947dd 100644 --- a/src/panels/developer-tools/action/developer-tools-action.ts +++ b/src/panels/developer-tools/action/developer-tools-action.ts @@ -25,7 +25,11 @@ import "../../../components/ha-service-picker"; import "../../../components/ha-yaml-editor"; import type { HaYamlEditor } from "../../../components/ha-yaml-editor"; import { forwardHaptic } from "../../../data/haptics"; -import { Action, ServiceAction } from "../../../data/script"; +import { + Action, + migrateAutomationAction, + ServiceAction, +} from "../../../data/script"; import { callExecuteScript, serviceCallWillDisconnect, @@ -49,14 +53,14 @@ class HaPanelDevAction extends LitElement { private _yamlValid = true; @storage({ - key: "panel-dev-service-state-service-data", + key: "panel-dev-action-state-service-data", state: true, subscribe: false, }) - private _serviceData?: ServiceAction = { service: "", target: {}, data: {} }; + private _serviceData?: ServiceAction = { action: "", target: {}, data: {} }; @storage({ - key: "panel-dev-service-state-yaml-mode", + key: "panel-dev-action-state-yaml-mode", state: true, subscribe: false, }) @@ -72,7 +76,7 @@ class HaPanelDevAction extends LitElement { const serviceParam = extractSearchParam("service"); if (serviceParam) { this._serviceData = { - service: serviceParam, + action: serviceParam, target: {}, data: {}, }; @@ -81,11 +85,11 @@ class HaPanelDevAction extends LitElement { this._yamlEditor?.setValue(this._serviceData) ); } - } else if (!this._serviceData?.service) { + } else if (!this._serviceData?.action) { const domain = Object.keys(this.hass.services).sort()[0]; const service = Object.keys(this.hass.services[domain]).sort()[0]; this._serviceData = { - service: `${domain}.${service}`, + action: `${domain}.${service}`, target: {}, data: {}, }; @@ -101,15 +105,15 @@ class HaPanelDevAction extends LitElement { protected render() { const { target, fields } = this._fields( this.hass.services, - this._serviceData?.service + this._serviceData?.action ); - const domain = this._serviceData?.service - ? computeDomain(this._serviceData?.service) + const domain = this._serviceData?.action + ? computeDomain(this._serviceData?.action) : undefined; - const serviceName = this._serviceData?.service - ? computeObjectId(this._serviceData?.service) + const serviceName = this._serviceData?.action + ? computeObjectId(this._serviceData?.action) : undefined; return html` @@ -124,7 +128,7 @@ class HaPanelDevAction extends LitElement { ? html`
- ${this._serviceData?.service + ${this._serviceData?.action ? html` { const errorCategory = yamlMode ? "yaml" : "ui"; - if (!serviceData?.service) { + if (!serviceData?.action) { return localize( - `ui.panel.developer-tools.tabs.actions.errors.${errorCategory}.no_service` + `ui.panel.developer-tools.tabs.actions.errors.${errorCategory}.no_action` ); } - const domain = computeDomain(serviceData.service); - const service = computeObjectId(serviceData.service); + const domain = computeDomain(serviceData.action); + const service = computeObjectId(serviceData.action); if (!domain || !service) { return localize( - `ui.panel.developer-tools.tabs.actions.errors.${errorCategory}.invalid_service` + `ui.panel.developer-tools.tabs.actions.errors.${errorCategory}.invalid_action` ); } if ( @@ -404,7 +408,7 @@ class HaPanelDevAction extends LitElement { const { target, fields } = this._fields( this.hass.services, - this._serviceData?.service + this._serviceData?.action ); this._error = this._validateServiceData( @@ -420,7 +424,7 @@ class HaPanelDevAction extends LitElement { button.actionError(); return; } - const [domain, service] = this._serviceData!.service!.split(".", 2); + const [domain, service] = this._serviceData!.action!.split(".", 2); const script: Action[] = []; if ( this.hass.services?.[domain]?.[service] && @@ -460,7 +464,7 @@ class HaPanelDevAction extends LitElement { this._error = localizedErrorMessage || this.hass.localize("ui.notification_toast.action_failed", { - service: this._serviceData!.service!, + service: this._serviceData!.action!, }) + ` ${err.message}`; return; } @@ -485,7 +489,7 @@ class HaPanelDevAction extends LitElement { private _checkUiSupported() { const fields = this._fields( this.hass.services, - this._serviceData?.service + this._serviceData?.action ).fields; if ( this._serviceData && @@ -512,16 +516,18 @@ class HaPanelDevAction extends LitElement { } private _serviceDataChanged(ev) { - if (this._serviceData?.service !== ev.detail.value.service) { + if (this._serviceData?.action !== ev.detail.value.action) { this._error = undefined; } - this._serviceData = ev.detail.value; + this._serviceData = migrateAutomationAction( + ev.detail.value + ) as ServiceAction; this._checkUiSupported(); } private _serviceChanged(ev) { ev.stopPropagation(); - this._serviceData = { service: ev.detail.value || "", data: {} }; + this._serviceData = { action: ev.detail.value || "", data: {} }; this._response = undefined; this._error = undefined; this._yamlEditor?.setValue(this._serviceData); @@ -531,14 +537,14 @@ class HaPanelDevAction extends LitElement { private _fillExampleData() { const { fields } = this._fields( this.hass.services, - this._serviceData?.service + this._serviceData?.action ); - const domain = this._serviceData?.service - ? computeDomain(this._serviceData?.service) + const domain = this._serviceData?.action + ? computeDomain(this._serviceData?.action) : undefined; - const serviceName = this._serviceData?.service - ? computeObjectId(this._serviceData?.service) + const serviceName = this._serviceData?.action + ? computeObjectId(this._serviceData?.action) : undefined; const example = {}; diff --git a/src/panels/lovelace/components/hui-action-editor.ts b/src/panels/lovelace/components/hui-action-editor.ts index 86ef25eb05..a13f1367db 100644 --- a/src/panels/lovelace/components/hui-action-editor.ts +++ b/src/panels/lovelace/components/hui-action-editor.ts @@ -103,7 +103,7 @@ export class HuiActionEditor extends LitElement { private _serviceAction = memoizeOne( (config: CallServiceActionConfig): ServiceAction => ({ - service: this._service, + action: this._service, ...(config.data || config.service_data ? { data: config.data ?? config.service_data } : null), diff --git a/src/translations/en.json b/src/translations/en.json index 2a2f595a4f..d706a8d066 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -6807,17 +6807,17 @@ "copy_clipboard_template": "Copy to clipboard (template)", "errors": { "ui": { - "no_service": "No action selected, please select an action", - "invalid_service": "Selected action is invalid, please select a valid action", + "no_action": "No action selected, please select an action", + "invalid_action": "Selected action is invalid, please select a valid action", "no_target": "This action requires a target, please select a target from the picker", "missing_required_field": "This action requires field {key}, please enter a valid value for {key}" }, "yaml": { "invalid_yaml": "Action YAML contains syntax errors, please fix the syntax", - "no_service": "No action defined, please define an action: key", - "invalid_service": "Defined action is invalid, please provide an action in the format domain.action", - "no_target": "This action requires a target, please define a target entity_id, device_id, or area_id under target: or data:", - "missing_required_field": "This action requires field {key}, which must be provided under data:" + "no_action": "No action defined, please define an 'action:' key", + "invalid_action": "Defined action is invalid, please provide an action in the format domain.action", + "no_target": "This action requires a target, please define a target 'entity_id', 'device_id', or 'area_id' under 'target:' or 'data:'", + "missing_required_field": "This action requires field {key}, which must be provided under 'data:'" } } },