mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-26 02:36:37 +00:00
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:
parent
6ccbeb8a75
commit
81c0bcff0b
@ -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" },
|
||||||
|
@ -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] },
|
||||||
];
|
];
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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: {},
|
||||||
},
|
},
|
||||||
|
@ -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";
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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";
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user