mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +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",
|
||||
},
|
||||
},
|
||||
{
|
||||
sequence: [
|
||||
{ scene: "scene.kitchen_morning" },
|
||||
{ service: "light.turn_off", target: { entity_id: "light.kitchen" } },
|
||||
],
|
||||
},
|
||||
{
|
||||
parallel: [
|
||||
{ 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 { 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] },
|
||||
];
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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: {},
|
||||
},
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
|
@ -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 }"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user