Allow and migrate action key in service action (#21503)

This commit is contained in:
Bram Kragten 2024-07-31 14:36:14 +02:00 committed by GitHub
parent da2e530601
commit 78becb5440
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 163 additions and 95 deletions

View File

@ -77,7 +77,7 @@ export class HaServiceControl extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: { @property({ attribute: false }) public value?: {
service: string; action: string;
target?: HassServiceTarget; target?: HassServiceTarget;
data?: Record<string, any>; data?: Record<string, any>;
}; };
@ -112,23 +112,23 @@ export class HaServiceControl extends LitElement {
| undefined | undefined
| this["value"]; | this["value"];
if (oldValue?.service !== this.value?.service) { if (oldValue?.action !== this.value?.action) {
this._checkedKeys = new Set(); this._checkedKeys = new Set();
} }
const serviceData = this._getServiceInfo( const serviceData = this._getServiceInfo(
this.value?.service, this.value?.action,
this.hass.services this.hass.services
); );
// Fetch the manifest if we have a service selected and the service domain changed. // Fetch the manifest if we have a service selected and the service domain changed.
// If no service is selected, clear the manifest. // If no service is selected, clear the manifest.
if (this.value?.service) { if (this.value?.action) {
if ( if (
!oldValue?.service || !oldValue?.action ||
computeDomain(this.value.service) !== computeDomain(oldValue.service) computeDomain(this.value.action) !== computeDomain(oldValue.action)
) { ) {
this._fetchManifest(computeDomain(this.value?.service)); this._fetchManifest(computeDomain(this.value?.action));
} }
} else { } else {
this._manifest = undefined; this._manifest = undefined;
@ -168,7 +168,7 @@ export class HaServiceControl extends LitElement {
this._value = this.value; this._value = this.value;
} }
if (oldValue?.service !== this.value?.service) { if (oldValue?.action !== this.value?.action) {
let updatedDefaultValue = false; let updatedDefaultValue = false;
if (this._value && serviceData) { if (this._value && serviceData) {
const loadDefaults = this.value && !("data" in this.value); const loadDefaults = this.value && !("data" in this.value);
@ -367,7 +367,7 @@ export class HaServiceControl extends LitElement {
protected render() { protected render() {
const serviceData = this._getServiceInfo( const serviceData = this._getServiceInfo(
this._value?.service, this._value?.action,
this.hass.services this.hass.services
); );
@ -392,11 +392,11 @@ export class HaServiceControl extends LitElement {
this._value this._value
); );
const domain = this._value?.service const domain = this._value?.action
? computeDomain(this._value.service) ? computeDomain(this._value.action)
: undefined; : undefined;
const serviceName = this._value?.service const serviceName = this._value?.action
? computeObjectId(this._value.service) ? computeObjectId(this._value.action)
: undefined; : undefined;
const description = const description =
@ -410,7 +410,7 @@ export class HaServiceControl extends LitElement {
? nothing ? nothing
: html`<ha-service-picker : html`<ha-service-picker
.hass=${this.hass} .hass=${this.hass}
.value=${this._value?.service} .value=${this._value?.action}
.disabled=${this.disabled} .disabled=${this.disabled}
@value-changed=${this._serviceChanged} @value-changed=${this._serviceChanged}
></ha-service-picker>`} ></ha-service-picker>`}
@ -596,11 +596,11 @@ export class HaServiceControl extends LitElement {
}; };
private _localizeValueCallback = (key: string) => { private _localizeValueCallback = (key: string) => {
if (!this._value?.service) { if (!this._value?.action) {
return ""; return "";
} }
return this.hass.localize( 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) { if (checked) {
this._checkedKeys.add(key); this._checkedKeys.add(key);
const field = this._getServiceInfo( const field = this._getServiceInfo(
this._value?.service, this._value?.action,
this.hass.services this.hass.services
)?.fields.find((_field) => _field.key === key); )?.fields.find((_field) => _field.key === key);
@ -658,7 +658,7 @@ export class HaServiceControl extends LitElement {
private _serviceChanged(ev: ValueChangedEvent<string>) { private _serviceChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation(); ev.stopPropagation();
if (ev.detail.value === this._value?.service) { if (ev.detail.value === this._value?.action) {
return; return;
} }

View File

@ -424,7 +424,7 @@ export class HatScriptGraph extends LitElement {
return html` return html`
<hat-graph-node <hat-graph-node
.graphStart=${graphStart} .graphStart=${graphStart}
.iconPath=${node.service ? undefined : mdiRoomService} .iconPath=${node.action ? undefined : mdiRoomService}
@focus=${this.selectNode(node, path)} @focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace} ?track=${path in this.trace.trace}
?active=${this.selected === path} ?active=${this.selected === path}
@ -432,11 +432,11 @@ export class HatScriptGraph extends LitElement {
.error=${this.trace.trace[path]?.some((tr) => tr.error)} .error=${this.trace.trace[path]?.some((tr) => tr.error)}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"} tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
> >
${node.service ${node.action
? html`<ha-service-icon ? html`<ha-service-icon
slot="icon" slot="icon"
.hass=${this.hass} .hass=${this.hass}
.service=${node.service} .service=${node.action}
></ha-service-icon>` ></ha-service-icon>`
: nothing} : nothing}
</hat-graph-node> </hat-graph-node>

View File

@ -6,7 +6,7 @@ import { navigate } from "../common/navigate";
import { Context, HomeAssistant } from "../types"; import { Context, HomeAssistant } from "../types";
import { BlueprintInput } from "./blueprint"; import { BlueprintInput } from "./blueprint";
import { DeviceCondition, DeviceTrigger } from "./device_automation"; 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_MODE: (typeof MODES)[number] = "single";
export const AUTOMATION_DEFAULT_MAX = 10; export const AUTOMATION_DEFAULT_MAX = 10;
@ -28,7 +28,7 @@ export interface ManualAutomationConfig {
description?: string; description?: string;
trigger: Trigger | Trigger[]; trigger: Trigger | Trigger[];
condition?: Condition | Condition[]; condition?: Condition | Condition[];
action: Action | Action[]; action?: Action | Action[];
mode?: (typeof MODES)[number]; mode?: (typeof MODES)[number];
max?: number; max?: number;
max_exceeded?: max_exceeded?:
@ -357,7 +357,7 @@ export const normalizeAutomationConfig = <
>( >(
config: T config: T
): 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 // Happens when people copy paste their automations into the config
for (const key of ["trigger", "condition", "action"]) { for (const key of ["trigger", "condition", "action"]) {
const value = config[key]; const value = config[key];
@ -365,6 +365,9 @@ export const normalizeAutomationConfig = <
config[key] = [value]; config[key] = [value];
} }
} }
config.action = migrateAutomationAction(config.action || []);
return config; return config;
}; };

View File

@ -49,7 +49,7 @@ const targetStruct = object({
export const serviceActionStruct: Describe<ServiceAction> = assign( export const serviceActionStruct: Describe<ServiceAction> = assign(
baseActionStruct, baseActionStruct,
object({ object({
service: 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(targetStruct),
@ -62,7 +62,7 @@ export const serviceActionStruct: Describe<ServiceAction> = assign(
const playMediaActionStruct: Describe<PlayMediaAction> = assign( const playMediaActionStruct: Describe<PlayMediaAction> = assign(
baseActionStruct, baseActionStruct,
object({ object({
service: literal("media_player.play_media"), action: literal("media_player.play_media"),
target: optional(object({ entity_id: optional(string()) })), target: optional(object({ entity_id: optional(string()) })),
entity_id: optional(string()), entity_id: optional(string()),
data: object({ media_content_id: string(), media_content_type: string() }), data: object({ media_content_id: string(), media_content_type: string() }),
@ -73,7 +73,7 @@ const playMediaActionStruct: Describe<PlayMediaAction> = assign(
const activateSceneActionStruct: Describe<ServiceSceneAction> = assign( const activateSceneActionStruct: Describe<ServiceSceneAction> = assign(
baseActionStruct, baseActionStruct,
object({ object({
service: literal("scene.turn_on"), action: literal("scene.turn_on"),
target: optional(object({ entity_id: optional(string()) })), target: optional(object({ entity_id: optional(string()) })),
entity_id: optional(string()), entity_id: optional(string()),
metadata: object(), metadata: object(),
@ -132,7 +132,7 @@ export interface EventAction extends BaseAction {
} }
export interface ServiceAction extends BaseAction { export interface ServiceAction extends BaseAction {
service?: string; action?: string;
service_template?: string; service_template?: string;
entity_id?: string; entity_id?: string;
target?: HassServiceTarget; target?: HassServiceTarget;
@ -160,7 +160,7 @@ export interface DelayAction extends BaseAction {
} }
export interface ServiceSceneAction extends BaseAction { export interface ServiceSceneAction extends BaseAction {
service: "scene.turn_on"; action: "scene.turn_on";
target?: { entity_id?: string }; target?: { entity_id?: string };
entity_id?: string; entity_id?: string;
metadata: Record<string, unknown>; metadata: Record<string, unknown>;
@ -191,7 +191,7 @@ export interface WaitForTriggerAction extends BaseAction {
} }
export interface PlayMediaAction extends BaseAction { export interface PlayMediaAction extends BaseAction {
service: "media_player.play_media"; action: "media_player.play_media";
target?: { entity_id?: string }; target?: { entity_id?: string };
entity_id?: string; entity_id?: string;
data: { media_content_id: string; media_content_type: 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) { if ("set_conversation_response" in action) {
return "set_conversation_response"; return "set_conversation_response";
} }
if ("service" in action) { if ("action" in action) {
if ("metadata" in action) { if ("metadata" in action) {
if (is(action, activateSceneActionStruct)) { if (is(action, activateSceneActionStruct)) {
return "activate_scene"; return "activate_scene";
@ -425,3 +425,60 @@ export const hasScriptFields = (
const fields = hass.services.script[computeObjectId(entityId)]?.fields; const fields = hass.services.script[computeObjectId(entityId)]?.fields;
return fields !== undefined && Object.keys(fields).length > 0; 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;
};

View File

@ -192,7 +192,7 @@ const tryDescribeAction = <T extends ActionType>(
if ( if (
config.service_template || config.service_template ||
(config.service && isTemplate(config.service)) (config.action && isTemplate(config.action))
) { ) {
return hass.localize( return hass.localize(
targets.length targets.length
@ -204,8 +204,8 @@ const tryDescribeAction = <T extends ActionType>(
); );
} }
if (config.service) { if (config.action) {
const [domain, serviceName] = config.service.split(".", 2); const [domain, serviceName] = config.action.split(".", 2);
const service = const service =
hass.localize(`component.${domain}.services.${serviceName}.name`) || hass.localize(`component.${domain}.services.${serviceName}.name`) ||
hass.services[domain][serviceName]?.name; hass.services[domain][serviceName]?.name;
@ -217,7 +217,7 @@ const tryDescribeAction = <T extends ActionType>(
: `${actionTranslationBaseKey}.service.description.service_name_no_targets`, : `${actionTranslationBaseKey}.service.description.service_name_no_targets`,
{ {
domain: domainToName(hass.localize, domain), domain: domainToName(hass.localize, domain),
name: service || config.service, name: service || config.action,
targets: formatListWithAnds(hass.locale, targets), targets: formatListWithAnds(hass.locale, targets),
} }
); );
@ -230,7 +230,7 @@ const tryDescribeAction = <T extends ActionType>(
{ {
name: service name: service
? `${domainToName(hass.localize, domain)}: ${service}` ? `${domainToName(hass.localize, domain)}: ${service}`
: config.service, : config.action,
targets: formatListWithAnds(hass.locale, targets), targets: formatListWithAnds(hass.locale, targets),
} }
); );

View File

@ -148,7 +148,7 @@ class MoreInfoScript extends LitElement {
const newState = this.stateObj; const newState = this.stateObj;
if (newState && (!oldState || oldState.entity_id !== newState.entity_id)) { if (newState && (!oldState || oldState.entity_id !== newState.entity_id)) {
this._scriptData = { service: newState.entity_id, data: {} }; this._scriptData = { action: newState.entity_id, data: {} };
} }
} }

View File

@ -87,8 +87,8 @@ export const getType = (action: Action | undefined) => {
if (!action) { if (!action) {
return undefined; return undefined;
} }
if ("service" in action || "scene" in action) { if ("action" in action || "scene" in action) {
return getActionType(action) as "activate_scene" | "service" | "play_media"; return getActionType(action) as "activate_scene" | "action" | "play_media";
} }
if (["and", "or", "not"].some((key) => key in action)) { if (["and", "or", "not"].some((key) => key in action)) {
return "condition" as const; return "condition" as const;
@ -214,12 +214,12 @@ export default class HaAutomationActionRow extends LitElement {
<ha-expansion-panel leftChevron> <ha-expansion-panel leftChevron>
<h3 slot="header"> <h3 slot="header">
${type === "service" && ${type === "service" &&
"service" in this.action && "action" in this.action &&
this.action.service this.action.action
? html`<ha-service-icon ? html`<ha-service-icon
class="action-icon" class="action-icon"
.hass=${this.hass} .hass=${this.hass}
.service=${this.action.service} .service=${this.action.action}
></ha-service-icon>` ></ha-service-icon>`
: html`<ha-svg-icon : html`<ha-svg-icon
class="action-icon" class="action-icon"

View File

@ -19,7 +19,7 @@ import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon"; import "../../../../components/ha-svg-icon";
import { getService, isService } from "../../../../data/action"; import { getService, isService } from "../../../../data/action";
import type { AutomationClipboard } from "../../../../data/automation"; import type { AutomationClipboard } from "../../../../data/automation";
import { Action } from "../../../../data/script"; import { Action, migrateAutomationAction } from "../../../../data/script";
import { HomeAssistant, ItemPath } from "../../../../types"; import { HomeAssistant, ItemPath } from "../../../../types";
import { import {
PASTE_VALUE, PASTE_VALUE,
@ -179,7 +179,7 @@ export default class HaAutomationAction extends LitElement {
actions = this.actions.concat(deepClone(this._clipboard!.action)); actions = this.actions.concat(deepClone(this._clipboard!.action));
} else if (isService(action)) { } else if (isService(action)) {
actions = this.actions.concat({ actions = this.actions.concat({
service: getService(action), action: getService(action),
metadata: {}, metadata: {},
}); });
} else { } else {
@ -243,7 +243,10 @@ export default class HaAutomationAction extends LitElement {
private _actionChanged(ev: CustomEvent) { private _actionChanged(ev: CustomEvent) {
ev.stopPropagation(); ev.stopPropagation();
const actions = [...this.actions]; const actions = [...this.actions];
const newValue = ev.detail.value; const newValue =
ev.detail.value === null
? ev.detail.value
: (migrateAutomationAction(ev.detail.value) as Action);
const index = (ev.target as any).index; const index = (ev.target as any).index;
if (newValue === null) { if (newValue === null) {

View File

@ -18,7 +18,7 @@ export class HaSceneAction extends LitElement implements ActionElement {
public static get defaultConfig(): SceneAction { public static get defaultConfig(): SceneAction {
return { return {
service: "scene.turn_on", action: "scene.turn_on",
target: { target: {
entity_id: "", entity_id: "",
}, },

View File

@ -20,7 +20,7 @@ export class HaPlayMediaAction extends LitElement implements ActionElement {
public static get defaultConfig(): PlayMediaAction { public static get defaultConfig(): PlayMediaAction {
return { return {
service: "media_player.play_media", action: "media_player.play_media",
target: { entity_id: "" }, target: { entity_id: "" },
data: { media_content_id: "", media_content_type: "" }, data: { media_content_id: "", media_content_type: "" },
metadata: {}, metadata: {},

View File

@ -67,10 +67,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
return; return;
} }
const fields = this._fields( const fields = this._fields(this.hass.services, this.action?.action).fields;
this.hass.services,
this.action?.service
).fields;
if ( if (
this.action && this.action &&
(Object.entries(this.action).some( (Object.entries(this.action).some(
@ -110,8 +107,8 @@ export class HaServiceAction extends LitElement implements ActionElement {
if (!this._action) { if (!this._action) {
return nothing; return nothing;
} }
const [domain, service] = this._action.service const [domain, service] = this._action.action
? this._action.service.split(".", 2) ? this._action.action.split(".", 2)
: [undefined, undefined]; : [undefined, undefined];
return html` return html`
<ha-service-control <ha-service-control
@ -168,8 +165,8 @@ export class HaServiceAction extends LitElement implements ActionElement {
} }
const value = { ...this.action, ...ev.detail.value }; const value = { ...this.action, ...ev.detail.value };
if ("response_variable" in this.action) { if ("response_variable" in this.action) {
const [domain, service] = this._action!.service const [domain, service] = this._action!.action
? this._action!.service.split(".", 2) ? this._action!.action.split(".", 2)
: [undefined, undefined]; : [undefined, undefined];
if ( if (
domain && domain &&
@ -181,6 +178,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
this._responseChecked = false; this._responseChecked = false;
} }
} }
fireEvent(this, "value-changed", { value }); fireEvent(this, "value-changed", { value });
} }

View File

@ -575,6 +575,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
private _valueChanged(ev: CustomEvent<{ value: AutomationConfig }>) { private _valueChanged(ev: CustomEvent<{ value: AutomationConfig }>) {
ev.stopPropagation(); ev.stopPropagation();
this._config = ev.detail.value; this._config = ev.detail.value;
if (this._readOnly) { if (this._readOnly) {
return; return;

View File

@ -25,7 +25,11 @@ import "../../../components/ha-service-picker";
import "../../../components/ha-yaml-editor"; import "../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../components/ha-yaml-editor"; import type { HaYamlEditor } from "../../../components/ha-yaml-editor";
import { forwardHaptic } from "../../../data/haptics"; import { forwardHaptic } from "../../../data/haptics";
import { Action, ServiceAction } from "../../../data/script"; import {
Action,
migrateAutomationAction,
ServiceAction,
} from "../../../data/script";
import { import {
callExecuteScript, callExecuteScript,
serviceCallWillDisconnect, serviceCallWillDisconnect,
@ -49,14 +53,14 @@ class HaPanelDevAction extends LitElement {
private _yamlValid = true; private _yamlValid = true;
@storage({ @storage({
key: "panel-dev-service-state-service-data", key: "panel-dev-action-state-service-data",
state: true, state: true,
subscribe: false, subscribe: false,
}) })
private _serviceData?: ServiceAction = { service: "", target: {}, data: {} }; private _serviceData?: ServiceAction = { action: "", target: {}, data: {} };
@storage({ @storage({
key: "panel-dev-service-state-yaml-mode", key: "panel-dev-action-state-yaml-mode",
state: true, state: true,
subscribe: false, subscribe: false,
}) })
@ -72,7 +76,7 @@ class HaPanelDevAction extends LitElement {
const serviceParam = extractSearchParam("service"); const serviceParam = extractSearchParam("service");
if (serviceParam) { if (serviceParam) {
this._serviceData = { this._serviceData = {
service: serviceParam, action: serviceParam,
target: {}, target: {},
data: {}, data: {},
}; };
@ -81,11 +85,11 @@ class HaPanelDevAction extends LitElement {
this._yamlEditor?.setValue(this._serviceData) 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 domain = Object.keys(this.hass.services).sort()[0];
const service = Object.keys(this.hass.services[domain]).sort()[0]; const service = Object.keys(this.hass.services[domain]).sort()[0];
this._serviceData = { this._serviceData = {
service: `${domain}.${service}`, action: `${domain}.${service}`,
target: {}, target: {},
data: {}, data: {},
}; };
@ -101,15 +105,15 @@ class HaPanelDevAction extends LitElement {
protected render() { protected render() {
const { target, fields } = this._fields( const { target, fields } = this._fields(
this.hass.services, this.hass.services,
this._serviceData?.service this._serviceData?.action
); );
const domain = this._serviceData?.service const domain = this._serviceData?.action
? computeDomain(this._serviceData?.service) ? computeDomain(this._serviceData?.action)
: undefined; : undefined;
const serviceName = this._serviceData?.service const serviceName = this._serviceData?.action
? computeObjectId(this._serviceData?.service) ? computeObjectId(this._serviceData?.action)
: undefined; : undefined;
return html` return html`
@ -124,7 +128,7 @@ class HaPanelDevAction extends LitElement {
? html`<div class="card-content"> ? html`<div class="card-content">
<ha-service-picker <ha-service-picker
.hass=${this.hass} .hass=${this.hass}
.value=${this._serviceData?.service} .value=${this._serviceData?.action}
@value-changed=${this._serviceChanged} @value-changed=${this._serviceChanged}
></ha-service-picker> ></ha-service-picker>
<ha-yaml-editor <ha-yaml-editor
@ -229,12 +233,12 @@ class HaPanelDevAction extends LitElement {
` `
: ""} : ""}
</h3> </h3>
${this._serviceData?.service ${this._serviceData?.action
? html` <a ? html` <a
href=${documentationUrl( href=${documentationUrl(
this.hass, this.hass,
"/integrations/" + "/integrations/" +
computeDomain(this._serviceData?.service) computeDomain(this._serviceData?.action)
)} )}
title=${this.hass.localize( title=${this.hass.localize(
"ui.components.service-control.integration_doc" "ui.components.service-control.integration_doc"
@ -316,23 +320,23 @@ class HaPanelDevAction extends LitElement {
); );
private _validateServiceData = ( private _validateServiceData = (
serviceData, serviceData: ServiceAction | undefined,
fields, fields,
target, target,
yamlMode: boolean, yamlMode: boolean,
localize: LocalizeFunc localize: LocalizeFunc
): string | undefined => { ): string | undefined => {
const errorCategory = yamlMode ? "yaml" : "ui"; const errorCategory = yamlMode ? "yaml" : "ui";
if (!serviceData?.service) { if (!serviceData?.action) {
return localize( 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 domain = computeDomain(serviceData.action);
const service = computeObjectId(serviceData.service); const service = computeObjectId(serviceData.action);
if (!domain || !service) { if (!domain || !service) {
return localize( return localize(
`ui.panel.developer-tools.tabs.actions.errors.${errorCategory}.invalid_service` `ui.panel.developer-tools.tabs.actions.errors.${errorCategory}.invalid_action`
); );
} }
if ( if (
@ -404,7 +408,7 @@ class HaPanelDevAction extends LitElement {
const { target, fields } = this._fields( const { target, fields } = this._fields(
this.hass.services, this.hass.services,
this._serviceData?.service this._serviceData?.action
); );
this._error = this._validateServiceData( this._error = this._validateServiceData(
@ -420,7 +424,7 @@ class HaPanelDevAction extends LitElement {
button.actionError(); button.actionError();
return; return;
} }
const [domain, service] = this._serviceData!.service!.split(".", 2); const [domain, service] = this._serviceData!.action!.split(".", 2);
const script: Action[] = []; const script: Action[] = [];
if ( if (
this.hass.services?.[domain]?.[service] && this.hass.services?.[domain]?.[service] &&
@ -460,7 +464,7 @@ class HaPanelDevAction extends LitElement {
this._error = this._error =
localizedErrorMessage || localizedErrorMessage ||
this.hass.localize("ui.notification_toast.action_failed", { this.hass.localize("ui.notification_toast.action_failed", {
service: this._serviceData!.service!, service: this._serviceData!.action!,
}) + ` ${err.message}`; }) + ` ${err.message}`;
return; return;
} }
@ -485,7 +489,7 @@ class HaPanelDevAction extends LitElement {
private _checkUiSupported() { private _checkUiSupported() {
const fields = this._fields( const fields = this._fields(
this.hass.services, this.hass.services,
this._serviceData?.service this._serviceData?.action
).fields; ).fields;
if ( if (
this._serviceData && this._serviceData &&
@ -512,16 +516,18 @@ class HaPanelDevAction extends LitElement {
} }
private _serviceDataChanged(ev) { private _serviceDataChanged(ev) {
if (this._serviceData?.service !== ev.detail.value.service) { if (this._serviceData?.action !== ev.detail.value.action) {
this._error = undefined; this._error = undefined;
} }
this._serviceData = ev.detail.value; this._serviceData = migrateAutomationAction(
ev.detail.value
) as ServiceAction;
this._checkUiSupported(); this._checkUiSupported();
} }
private _serviceChanged(ev) { private _serviceChanged(ev) {
ev.stopPropagation(); ev.stopPropagation();
this._serviceData = { service: ev.detail.value || "", data: {} }; this._serviceData = { action: ev.detail.value || "", data: {} };
this._response = undefined; this._response = undefined;
this._error = undefined; this._error = undefined;
this._yamlEditor?.setValue(this._serviceData); this._yamlEditor?.setValue(this._serviceData);
@ -531,14 +537,14 @@ class HaPanelDevAction extends LitElement {
private _fillExampleData() { private _fillExampleData() {
const { fields } = this._fields( const { fields } = this._fields(
this.hass.services, this.hass.services,
this._serviceData?.service this._serviceData?.action
); );
const domain = this._serviceData?.service const domain = this._serviceData?.action
? computeDomain(this._serviceData?.service) ? computeDomain(this._serviceData?.action)
: undefined; : undefined;
const serviceName = this._serviceData?.service const serviceName = this._serviceData?.action
? computeObjectId(this._serviceData?.service) ? computeObjectId(this._serviceData?.action)
: undefined; : undefined;
const example = {}; const example = {};

View File

@ -103,7 +103,7 @@ export class HuiActionEditor extends LitElement {
private _serviceAction = memoizeOne( private _serviceAction = memoizeOne(
(config: CallServiceActionConfig): ServiceAction => ({ (config: CallServiceActionConfig): ServiceAction => ({
service: this._service, action: this._service,
...(config.data || config.service_data ...(config.data || config.service_data
? { data: config.data ?? config.service_data } ? { data: config.data ?? config.service_data }
: null), : null),

View File

@ -6807,17 +6807,17 @@
"copy_clipboard_template": "Copy to clipboard (template)", "copy_clipboard_template": "Copy to clipboard (template)",
"errors": { "errors": {
"ui": { "ui": {
"no_service": "No action selected, please select an action", "no_action": "No action selected, please select an action",
"invalid_service": "Selected action is invalid, please select a valid 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", "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}" "missing_required_field": "This action requires field {key}, please enter a valid value for {key}"
}, },
"yaml": { "yaml": {
"invalid_yaml": "Action YAML contains syntax errors, please fix the syntax", "invalid_yaml": "Action YAML contains syntax errors, please fix the syntax",
"no_service": "No action defined, please define an action: key", "no_action": "No action defined, please define an 'action:' key",
"invalid_service": "Defined action is invalid, please provide an action in the format domain.action", "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:", "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:" "missing_required_field": "This action requires field {key}, which must be provided under 'data:'"
} }
} }
}, },