diff --git a/gallery/src/pages/automation/describe-action.ts b/gallery/src/pages/automation/describe-action.ts index 3340429dc4..c579ed6aaa 100644 --- a/gallery/src/pages/automation/describe-action.ts +++ b/gallery/src/pages/automation/describe-action.ts @@ -64,6 +64,12 @@ const ACTIONS = [ entity_id: "input_boolean.toggle_4", }, }, + { + sequence: [ + { scene: "scene.kitchen_morning" }, + { service: "light.turn_off", target: { entity_id: "light.kitchen" } }, + ], + }, { parallel: [ { scene: "scene.kitchen_morning" }, diff --git a/gallery/src/pages/automation/editor-action.ts b/gallery/src/pages/automation/editor-action.ts index 28f9bb16e2..fb7ab5fc2b 100644 --- a/gallery/src/pages/automation/editor-action.ts +++ b/gallery/src/pages/automation/editor-action.ts @@ -20,6 +20,7 @@ import { HaWaitForTriggerAction } from "../../../../src/panels/config/automation import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template"; import { Action } from "../../../../src/data/script"; import { HaConditionAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-condition"; +import { HaSequenceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-sequence"; import { HaParallelAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-parallel"; import { HaIfAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-if"; import { HaStopAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-stop"; @@ -39,6 +40,7 @@ const SCHEMAS: { name: string; actions: Action[] }[] = [ { name: "If-Then", actions: [HaIfAction.defaultConfig] }, { name: "Choose", actions: [HaChooseAction.defaultConfig] }, { name: "Variables", actions: [{ variables: { hello: "1" } }] }, + { name: "Sequence", actions: [HaSequenceAction.defaultConfig] }, { name: "Parallel", actions: [HaParallelAction.defaultConfig] }, { name: "Stop", actions: [HaStopAction.defaultConfig] }, ]; diff --git a/src/components/trace/hat-script-graph.ts b/src/components/trace/hat-script-graph.ts index 077888293a..f9aaf73050 100644 --- a/src/components/trace/hat-script-graph.ts +++ b/src/components/trace/hat-script-graph.ts @@ -13,6 +13,7 @@ import { mdiClose, mdiCodeBraces, mdiCodeBrackets, + mdiFormatListNumbered, mdiRefresh, mdiRoomService, mdiShuffleDisabled, @@ -29,6 +30,7 @@ import { ManualScriptConfig, ParallelAction, RepeatAction, + SequenceAction, ServiceAction, WaitAction, WaitForTriggerAction, @@ -119,6 +121,7 @@ export class HatScriptGraph extends LitElement { repeat: this.render_repeat_node, choose: this.render_choose_node, if: this.render_if_node, + sequence: this.render_sequence_node, parallel: this.render_parallel_node, other: this.render_other_node, }; @@ -460,6 +463,44 @@ export class HatScriptGraph extends LitElement { `; } + private render_sequence_node( + node: SequenceAction, + path: string, + graphStart = false, + disabled = false + ) { + const trace: any = this.trace.trace[path]; + return html` + +
+ + ${ensureArray(node.sequence).map((action, i) => + this.render_action_node( + action, + `${path}/sequence/${i}`, + false, + disabled || node.enabled === false + ) + )} +
+
+ `; + } + private render_parallel_node( node: ParallelAction, path: string, diff --git a/src/components/trace/hat-trace-timeline.ts b/src/components/trace/hat-trace-timeline.ts index 9a440e6ed1..e6be959c92 100644 --- a/src/components/trace/hat-trace-timeline.ts +++ b/src/components/trace/hat-trace-timeline.ts @@ -37,6 +37,7 @@ import { IfAction, ParallelAction, RepeatAction, + SequenceAction, getActionType, } from "../../data/script"; import { describeAction } from "../../data/script_i18n"; @@ -310,6 +311,10 @@ class ActionRenderer { return this._handleIf(index); } + if (actionType === "sequence") { + return this._handleSequence(index); + } + if (actionType === "parallel") { return this._handleParallel(index); } @@ -579,6 +584,37 @@ class ActionRenderer { return i; } + private _handleSequence(index: number): number { + const sequencePath = this.keys[index]; + const sequenceConfig = this._getDataFromPath( + this.keys[index] + ) as SequenceAction; + + this._renderEntry( + sequencePath, + sequenceConfig.alias || + describeAction( + this.hass, + this.entityReg, + this.labelReg, + this.floorReg, + sequenceConfig, + "sequence" + ), + undefined, + sequenceConfig.enabled === false + ); + + let i: number; + + for (i = index + 1; i < this.keys.length; i++) { + const path = this.keys[i]; + this._renderItem(i, getActionType(this._getDataFromPath(path))); + } + + return i; + } + private _handleParallel(index: number): number { const parallelPath = this.keys[index]; const startLevel = parallelPath.split("/").length; diff --git a/src/data/action.ts b/src/data/action.ts index c85d5226c8..69a5f9a869 100644 --- a/src/data/action.ts +++ b/src/data/action.ts @@ -8,6 +8,7 @@ import { mdiDevices, mdiDotsHorizontal, mdiExcavator, + mdiFormatListNumbered, mdiGestureDoubleTap, mdiHandBackRight, mdiPalette, @@ -35,6 +36,7 @@ export const ACTION_ICONS = { if: mdiCallSplit, device_id: mdiDevices, stop: mdiHandBackRight, + sequence: mdiFormatListNumbered, parallel: mdiShuffleDisabled, variables: mdiApplicationVariableOutline, set_conversation_response: mdiBullhorn, @@ -61,6 +63,7 @@ export const ACTION_GROUPS: AutomationElementGroup = { choose: {}, if: {}, stop: {}, + sequence: {}, parallel: {}, variables: {}, }, diff --git a/src/data/script.ts b/src/data/script.ts index 28aff64dcb..f70ca2d338 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -248,6 +248,10 @@ export interface StopAction extends BaseAction { error?: boolean; } +export interface SequenceAction extends BaseAction { + sequence: (ManualScriptConfig | Action)[]; +} + export interface ParallelAction extends BaseAction { parallel: ManualScriptConfig | Action | (ManualScriptConfig | Action)[]; } @@ -274,6 +278,7 @@ export type NonConditionAction = | VariablesAction | PlayMediaAction | StopAction + | SequenceAction | ParallelAction | UnknownAction; @@ -299,6 +304,7 @@ export interface ActionTypes { service: ServiceAction; play_media: PlayMediaAction; stop: StopAction; + sequence: SequenceAction; parallel: ParallelAction; set_conversation_response: SetConversationResponseAction; unknown: UnknownAction; @@ -389,6 +395,9 @@ export const getActionType = (action: Action): ActionType => { if ("stop" in action) { return "stop"; } + if ("sequence" in action) { + return "sequence"; + } if ("parallel" in action) { return "parallel"; } diff --git a/src/data/script_i18n.ts b/src/data/script_i18n.ts index 686b52438e..4d518bd57c 100644 --- a/src/data/script_i18n.ts +++ b/src/data/script_i18n.ts @@ -29,6 +29,7 @@ import { PlayMediaAction, RepeatAction, SceneAction, + SequenceAction, SetConversationResponseAction, StopAction, VariablesAction, @@ -478,6 +479,15 @@ const tryDescribeAction = ( }`; } + if (actionType === "sequence") { + const config = action as SequenceAction; + const numActions = ensureArray(config.sequence).length; + return hass.localize( + `${actionTranslationBaseKey}.sequence.description.full`, + { number: numActions } + ); + } + if (actionType === "parallel") { const config = action as ParallelAction; const numActions = ensureArray(config.parallel).length; 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 bac8c011fb..fc43b6a3ca 100644 --- a/src/panels/config/automation/action/ha-automation-action-row.ts +++ b/src/panels/config/automation/action/ha-automation-action-row.ts @@ -72,6 +72,7 @@ import "./types/ha-automation-action-delay"; import "./types/ha-automation-action-device_id"; import "./types/ha-automation-action-event"; import "./types/ha-automation-action-if"; +import "./types/ha-automation-action-sequence"; import "./types/ha-automation-action-parallel"; import "./types/ha-automation-action-play_media"; import "./types/ha-automation-action-repeat"; diff --git a/src/panels/config/automation/action/types/ha-automation-action-sequence.ts b/src/panels/config/automation/action/types/ha-automation-action-sequence.ts new file mode 100644 index 0000000000..d8621dafa2 --- /dev/null +++ b/src/panels/config/automation/action/types/ha-automation-action-sequence.ts @@ -0,0 +1,67 @@ +import { CSSResultGroup, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import "../../../../../components/ha-textfield"; +import { Action, SequenceAction } from "../../../../../data/script"; +import { haStyle } from "../../../../../resources/styles"; +import type { HomeAssistant, ItemPath } from "../../../../../types"; +import "../ha-automation-action"; +import type { ActionElement } from "../ha-automation-action-row"; + +@customElement("ha-automation-action-sequence") +export class HaSequenceAction extends LitElement implements ActionElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public disabled = false; + + @property({ attribute: false }) public path?: ItemPath; + + @property({ attribute: false }) public action!: SequenceAction; + + public static get defaultConfig() { + return { + sequence: [], + }; + } + + private _getMemoizedPath = memoizeOne((path: ItemPath | undefined) => [ + ...(path ?? []), + "sequence", + ]); + + protected render() { + const { action } = this; + + return html` + + `; + } + + private _actionsChanged(ev: CustomEvent) { + ev.stopPropagation(); + const value = ev.detail.value as Action[]; + fireEvent(this, "value-changed", { + value: { + ...this.action, + sequence: value, + }, + }); + } + + static get styles(): CSSResultGroup { + return haStyle; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-action-sequence": HaSequenceAction; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index 0887761469..508c709668 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3418,10 +3418,17 @@ "full": "Stop {hasReason, select, \n true { because: {reason}} \n other {}\n }" } }, + "sequence": { + "label": "Run in sequence", + "description": { + "picker": "Run a group of actions in sequence.", + "full": "Run {number} {number, plural,\n one {action}\n other {actions}\n} in sequence" + } + }, "parallel": { "label": "Run in parallel", "description": { - "picker": "Perform a sequence of actions in parallel.", + "picker": "Perform actions in parallel.", "full": "Run {number} {number, plural,\n one {action}\n other {actions}\n} in parallel" } },