Add sequence action building block (#20874)

* Add sequence action building block

* This is a non-conditional action

* Render sequence in automation traces graph

* Render trace timeline

* Process review comment
This commit is contained in:
Franck Nijhof 2024-05-26 20:54:05 +02:00 committed by GitHub
parent 6ccbeb8a75
commit 81c0bcff0b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 183 additions and 1 deletions

View File

@ -64,6 +64,12 @@ const ACTIONS = [
entity_id: "input_boolean.toggle_4", entity_id: "input_boolean.toggle_4",
}, },
}, },
{
sequence: [
{ scene: "scene.kitchen_morning" },
{ service: "light.turn_off", target: { entity_id: "light.kitchen" } },
],
},
{ {
parallel: [ parallel: [
{ scene: "scene.kitchen_morning" }, { scene: "scene.kitchen_morning" },

View File

@ -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 { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template";
import { Action } from "../../../../src/data/script"; import { Action } from "../../../../src/data/script";
import { HaConditionAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-condition"; 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 { 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 { 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"; 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: "If-Then", actions: [HaIfAction.defaultConfig] },
{ name: "Choose", actions: [HaChooseAction.defaultConfig] }, { name: "Choose", actions: [HaChooseAction.defaultConfig] },
{ name: "Variables", actions: [{ variables: { hello: "1" } }] }, { name: "Variables", actions: [{ variables: { hello: "1" } }] },
{ name: "Sequence", actions: [HaSequenceAction.defaultConfig] },
{ name: "Parallel", actions: [HaParallelAction.defaultConfig] }, { name: "Parallel", actions: [HaParallelAction.defaultConfig] },
{ name: "Stop", actions: [HaStopAction.defaultConfig] }, { name: "Stop", actions: [HaStopAction.defaultConfig] },
]; ];

View File

@ -13,6 +13,7 @@ import {
mdiClose, mdiClose,
mdiCodeBraces, mdiCodeBraces,
mdiCodeBrackets, mdiCodeBrackets,
mdiFormatListNumbered,
mdiRefresh, mdiRefresh,
mdiRoomService, mdiRoomService,
mdiShuffleDisabled, mdiShuffleDisabled,
@ -29,6 +30,7 @@ import {
ManualScriptConfig, ManualScriptConfig,
ParallelAction, ParallelAction,
RepeatAction, RepeatAction,
SequenceAction,
ServiceAction, ServiceAction,
WaitAction, WaitAction,
WaitForTriggerAction, WaitForTriggerAction,
@ -119,6 +121,7 @@ export class HatScriptGraph extends LitElement {
repeat: this.render_repeat_node, repeat: this.render_repeat_node,
choose: this.render_choose_node, choose: this.render_choose_node,
if: this.render_if_node, if: this.render_if_node,
sequence: this.render_sequence_node,
parallel: this.render_parallel_node, parallel: this.render_parallel_node,
other: this.render_other_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`
<hat-graph-branch
tabindex=${trace === undefined ? "-1" : "0"}
@focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
>
<div class="graph-container" ?track=${path in this.trace.trace}>
<hat-graph-node
.graphStart=${graphStart}
.iconPath=${mdiFormatListNumbered}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
slot="head"
nofocus
></hat-graph-node>
${ensureArray(node.sequence).map((action, i) =>
this.render_action_node(
action,
`${path}/sequence/${i}`,
false,
disabled || node.enabled === false
)
)}
</div>
</hat-graph-branch>
`;
}
private render_parallel_node( private render_parallel_node(
node: ParallelAction, node: ParallelAction,
path: string, path: string,

View File

@ -37,6 +37,7 @@ import {
IfAction, IfAction,
ParallelAction, ParallelAction,
RepeatAction, RepeatAction,
SequenceAction,
getActionType, getActionType,
} from "../../data/script"; } from "../../data/script";
import { describeAction } from "../../data/script_i18n"; import { describeAction } from "../../data/script_i18n";
@ -310,6 +311,10 @@ class ActionRenderer {
return this._handleIf(index); return this._handleIf(index);
} }
if (actionType === "sequence") {
return this._handleSequence(index);
}
if (actionType === "parallel") { if (actionType === "parallel") {
return this._handleParallel(index); return this._handleParallel(index);
} }
@ -579,6 +584,37 @@ class ActionRenderer {
return i; 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 { private _handleParallel(index: number): number {
const parallelPath = this.keys[index]; const parallelPath = this.keys[index];
const startLevel = parallelPath.split("/").length; const startLevel = parallelPath.split("/").length;

View File

@ -8,6 +8,7 @@ import {
mdiDevices, mdiDevices,
mdiDotsHorizontal, mdiDotsHorizontal,
mdiExcavator, mdiExcavator,
mdiFormatListNumbered,
mdiGestureDoubleTap, mdiGestureDoubleTap,
mdiHandBackRight, mdiHandBackRight,
mdiPalette, mdiPalette,
@ -35,6 +36,7 @@ export const ACTION_ICONS = {
if: mdiCallSplit, if: mdiCallSplit,
device_id: mdiDevices, device_id: mdiDevices,
stop: mdiHandBackRight, stop: mdiHandBackRight,
sequence: mdiFormatListNumbered,
parallel: mdiShuffleDisabled, parallel: mdiShuffleDisabled,
variables: mdiApplicationVariableOutline, variables: mdiApplicationVariableOutline,
set_conversation_response: mdiBullhorn, set_conversation_response: mdiBullhorn,
@ -61,6 +63,7 @@ export const ACTION_GROUPS: AutomationElementGroup = {
choose: {}, choose: {},
if: {}, if: {},
stop: {}, stop: {},
sequence: {},
parallel: {}, parallel: {},
variables: {}, variables: {},
}, },

View File

@ -248,6 +248,10 @@ export interface StopAction extends BaseAction {
error?: boolean; error?: boolean;
} }
export interface SequenceAction extends BaseAction {
sequence: (ManualScriptConfig | Action)[];
}
export interface ParallelAction extends BaseAction { export interface ParallelAction extends BaseAction {
parallel: ManualScriptConfig | Action | (ManualScriptConfig | Action)[]; parallel: ManualScriptConfig | Action | (ManualScriptConfig | Action)[];
} }
@ -274,6 +278,7 @@ export type NonConditionAction =
| VariablesAction | VariablesAction
| PlayMediaAction | PlayMediaAction
| StopAction | StopAction
| SequenceAction
| ParallelAction | ParallelAction
| UnknownAction; | UnknownAction;
@ -299,6 +304,7 @@ export interface ActionTypes {
service: ServiceAction; service: ServiceAction;
play_media: PlayMediaAction; play_media: PlayMediaAction;
stop: StopAction; stop: StopAction;
sequence: SequenceAction;
parallel: ParallelAction; parallel: ParallelAction;
set_conversation_response: SetConversationResponseAction; set_conversation_response: SetConversationResponseAction;
unknown: UnknownAction; unknown: UnknownAction;
@ -389,6 +395,9 @@ export const getActionType = (action: Action): ActionType => {
if ("stop" in action) { if ("stop" in action) {
return "stop"; return "stop";
} }
if ("sequence" in action) {
return "sequence";
}
if ("parallel" in action) { if ("parallel" in action) {
return "parallel"; return "parallel";
} }

View File

@ -29,6 +29,7 @@ import {
PlayMediaAction, PlayMediaAction,
RepeatAction, RepeatAction,
SceneAction, SceneAction,
SequenceAction,
SetConversationResponseAction, SetConversationResponseAction,
StopAction, StopAction,
VariablesAction, VariablesAction,
@ -478,6 +479,15 @@ const tryDescribeAction = <T extends ActionType>(
}`; }`;
} }
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") { if (actionType === "parallel") {
const config = action as ParallelAction; const config = action as ParallelAction;
const numActions = ensureArray(config.parallel).length; const numActions = ensureArray(config.parallel).length;

View File

@ -72,6 +72,7 @@ import "./types/ha-automation-action-delay";
import "./types/ha-automation-action-device_id"; import "./types/ha-automation-action-device_id";
import "./types/ha-automation-action-event"; import "./types/ha-automation-action-event";
import "./types/ha-automation-action-if"; import "./types/ha-automation-action-if";
import "./types/ha-automation-action-sequence";
import "./types/ha-automation-action-parallel"; import "./types/ha-automation-action-parallel";
import "./types/ha-automation-action-play_media"; import "./types/ha-automation-action-play_media";
import "./types/ha-automation-action-repeat"; import "./types/ha-automation-action-repeat";

View File

@ -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`
<ha-automation-action
.path=${this._getMemoizedPath(this.path)}
.actions=${action.sequence}
.disabled=${this.disabled}
@value-changed=${this._actionsChanged}
.hass=${this.hass}
></ha-automation-action>
`;
}
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;
}
}

View File

@ -3418,10 +3418,17 @@
"full": "Stop {hasReason, select, \n true { because: {reason}} \n other {}\n }" "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": { "parallel": {
"label": "Run in parallel", "label": "Run in parallel",
"description": { "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" "full": "Run {number} {number, plural,\n one {action}\n other {actions}\n} in parallel"
} }
}, },