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",
},
},
{
sequence: [
{ scene: "scene.kitchen_morning" },
{ service: "light.turn_off", target: { entity_id: "light.kitchen" } },
],
},
{
parallel: [
{ 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 { 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] },
];

View File

@ -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`
<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(
node: ParallelAction,
path: string,

View File

@ -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;

View File

@ -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: {},
},

View File

@ -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";
}

View File

@ -29,6 +29,7 @@ import {
PlayMediaAction,
RepeatAction,
SceneAction,
SequenceAction,
SetConversationResponseAction,
StopAction,
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") {
const config = action as ParallelAction;
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-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";

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 }"
}
},
"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"
}
},