diff --git a/src/data/automation.ts b/src/data/automation.ts index 4b5292b2138e..ea20b874c811 100644 --- a/src/data/automation.ts +++ b/src/data/automation.ts @@ -352,6 +352,22 @@ export const saveAutomationConfig = ( config: AutomationConfig ) => hass.callApi("POST", `config/automation/config/${id}`, config); +export const normalizeAutomationConfig = < + T extends Partial | AutomationConfig, +>( + config: T +): T => { + // Normalize data: ensure trigger, action and condition are lists + // Happens when people copy paste their automations into the config + for (const key of ["trigger", "condition", "action"]) { + const value = config[key]; + if (value && !Array.isArray(value)) { + config[key] = [value]; + } + } + return config; +}; + export const showAutomationEditor = (data?: Partial) => { initialAutomationEditorData = data; navigate("/config/automation/edit/new"); diff --git a/src/data/blueprint.ts b/src/data/blueprint.ts index 222da542af7a..a0e9f2562da3 100644 --- a/src/data/blueprint.ts +++ b/src/data/blueprint.ts @@ -1,4 +1,6 @@ import { HomeAssistant } from "../types"; +import { ManualAutomationConfig } from "./automation"; +import { ManualScriptConfig } from "./script"; import { Selector } from "./selector"; export type BlueprintDomain = "automation" | "script"; @@ -42,6 +44,11 @@ export interface BlueprintImportResult { validation_errors: string[] | null; } +export interface BlueprintSubstituteResults { + automation: { substituted_config: ManualAutomationConfig }; + script: { substituted_config: ManualScriptConfig }; +} + export const fetchBlueprints = (hass: HomeAssistant, domain: BlueprintDomain) => hass.callWS({ type: "blueprint/list", domain }); @@ -91,3 +98,18 @@ export const getBlueprintSourceType = ( } return "community"; }; + +export const substituteBlueprint = < + T extends BlueprintDomain = BlueprintDomain, +>( + hass: HomeAssistant, + domain: T, + path: string, + input: Record +) => + hass.callWS({ + type: "blueprint/substitute", + domain, + path, + input, + }); diff --git a/src/panels/config/automation/blueprint-automation-editor.ts b/src/panels/config/automation/blueprint-automation-editor.ts index 83a7c7bb991a..959230be5f97 100644 --- a/src/panels/config/automation/blueprint-automation-editor.ts +++ b/src/panels/config/automation/blueprint-automation-editor.ts @@ -3,10 +3,10 @@ import { HassEntity } from "home-assistant-js-websocket"; import { html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import "../../../components/ha-alert"; +import "../../../components/ha-markdown"; import { BlueprintAutomationConfig } from "../../../data/automation"; import { fetchBlueprints } from "../../../data/blueprint"; import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor"; -import "../../../components/ha-markdown"; @customElement("blueprint-automation-editor") export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor { diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index 0cfac00e717b..939c00af531c 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -6,6 +6,7 @@ import { mdiDebugStepOver, mdiDelete, mdiDotsVertical, + mdiFileEdit, mdiInformationOutline, mdiPlay, mdiPlayCircleOutline, @@ -40,10 +41,12 @@ import "../../../components/ha-yaml-editor"; import { AutomationConfig, AutomationEntity, + BlueprintAutomationConfig, deleteAutomation, fetchAutomationFileConfig, getAutomationEditorInitData, getAutomationStateConfig, + normalizeAutomationConfig, saveAutomationConfig, showAutomationEditor, triggerAutomationActions, @@ -65,6 +68,7 @@ import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-a import { showAutomationRenameDialog } from "./automation-rename-dialog/show-dialog-automation-rename"; import "./blueprint-automation-editor"; import "./manual-automation-editor"; +import { substituteBlueprint } from "../../../data/blueprint"; declare global { interface HTMLElementTagNameMap { @@ -235,6 +239,24 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { > + ${useBlueprint + ? html` + + ${this.hass.localize( + "ui.panel.config.automation.editor.take_control" + )} + + + ` + : nothing} +
  • @@ -432,7 +454,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { } this._config = { ...baseConfig, - ...initData, + ...(initData ? normalizeAutomationConfig(initData) : initData), } as AutomationConfig; this._entityId = undefined; this._readOnly = false; @@ -441,7 +463,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { if (changedProps.has("entityId") && this.entityId) { getAutomationStateConfig(this.hass, this.entityId).then((c) => { - this._config = this._normalizeConfig(c.config); + this._config = normalizeAutomationConfig(c.config); this._checkValidation(); }); this._entityId = this.entityId; @@ -497,18 +519,6 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { ); } - private _normalizeConfig(config: AutomationConfig): AutomationConfig { - // Normalize data: ensure trigger, action and condition are lists - // Happens when people copy paste their automations into the config - for (const key of ["trigger", "condition", "action"]) { - const value = config[key]; - if (value && !Array.isArray(value)) { - config[key] = [value]; - } - } - return config; - } - private async _loadConfig() { try { const config = await fetchAutomationFileConfig( @@ -517,7 +527,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { ); this._dirty = false; this._readOnly = false; - this._config = this._normalizeConfig(config); + this._config = normalizeAutomationConfig(config); this._checkValidation(); } catch (err: any) { const entityRegistry = await fetchEntityRegistry(this.hass.connection); @@ -638,6 +648,45 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { } }; + private async _takeControl() { + const config = this._config as BlueprintAutomationConfig; + + const confirmation = await showConfirmationDialog(this, { + title: this.hass!.localize( + "ui.panel.config.automation.editor.take_control_confirmation.title" + ), + text: this.hass!.localize( + "ui.panel.config.automation.editor.take_control_confirmation.text" + ), + confirmText: this.hass!.localize( + "ui.panel.config.automation.editor.take_control_confirmation.action" + ), + }); + + if (!confirmation) return; + + try { + const result = await substituteBlueprint( + this.hass, + "automation", + config.use_blueprint.path, + config.use_blueprint.input || {} + ); + + const newConfig = { + ...normalizeAutomationConfig(result.substituted_config), + alias: config.alias, + description: config.description, + }; + + this._config = newConfig; + this._dirty = true; + this._errors = undefined; + } catch (err: any) { + this._errors = err.message; + } + } + private async _duplicate() { const result = this._readOnly ? await showConfirmationDialog(this, { diff --git a/src/panels/config/script/blueprint-script-editor.ts b/src/panels/config/script/blueprint-script-editor.ts index ba388f5e0d75..bc7b624a0cb3 100644 --- a/src/panels/config/script/blueprint-script-editor.ts +++ b/src/panels/config/script/blueprint-script-editor.ts @@ -2,10 +2,10 @@ import "@material/mwc-button/mwc-button"; import { html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import "../../../components/ha-alert"; -import { BlueprintScriptConfig } from "../../../data/script"; +import "../../../components/ha-markdown"; import { fetchBlueprints } from "../../../data/blueprint"; +import { BlueprintScriptConfig } from "../../../data/script"; import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor"; -import "../../../components/ha-markdown"; @customElement("blueprint-script-editor") export class HaBlueprintScriptEditor extends HaBlueprintGenericEditor { diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index 3f12a64f5563..c11a0dc524eb 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -6,6 +6,7 @@ import { mdiDebugStepOver, mdiDelete, mdiDotsVertical, + mdiFileEdit, mdiFormTextbox, mdiInformationOutline, mdiPlay, @@ -40,6 +41,7 @@ import { validateConfig } from "../../../data/config"; import { UNAVAILABLE } from "../../../data/entity"; import { EntityRegistryEntry } from "../../../data/entity_registry"; import { + BlueprintScriptConfig, ScriptConfig, deleteScript, fetchScriptFileConfig, @@ -61,6 +63,7 @@ import { showAutomationRenameDialog } from "../automation/automation-rename-dial import "./blueprint-script-editor"; import "./manual-script-editor"; import type { HaManualScriptEditor } from "./manual-script-editor"; +import { substituteBlueprint } from "../../../data/blueprint"; export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @@ -228,6 +231,24 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { > + ${useBlueprint + ? html` + + ${this.hass.localize( + "ui.panel.config.script.editor.take_control" + )} + + + ` + : nothing} +
  • @@ -601,6 +622,45 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { } }; + private async _takeControl() { + const config = this._config as BlueprintScriptConfig; + + const confirmation = await showConfirmationDialog(this, { + title: this.hass!.localize( + "ui.panel.config.script.editor.take_control_confirmation.title" + ), + text: this.hass!.localize( + "ui.panel.config.script.editor.take_control_confirmation.text" + ), + confirmText: this.hass!.localize( + "ui.panel.config.script.editor.take_control_confirmation.action" + ), + }); + + if (!confirmation) return; + + try { + const result = await substituteBlueprint( + this.hass, + "script", + config.use_blueprint.path, + config.use_blueprint.input || {} + ); + + const newConfig = { + ...this._normalizeConfig(result.substituted_config), + alias: config.alias, + description: config.description, + }; + + this._config = newConfig; + this._dirty = true; + this._errors = undefined; + } catch (err: any) { + this._errors = err.message; + } + } + private async _duplicate() { const result = this._readOnly ? await showConfirmationDialog(this, { diff --git a/src/translations/en.json b/src/translations/en.json index 2cf298b59873..f813a386f136 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2759,6 +2759,12 @@ "unavailable": "Automation is unavailable", "migrate": "Migrate", "duplicate": "[%key:ui::common::duplicate%]", + "take_control": "Take control", + "take_control_confirmation": { + "title": "Take control of automation?", + "text": "This automation is using a blueprint. By taking control, your automation will be converted into a regular automation using triggers, conditions and actions. You will be able to edit it directly and you won't be able to convert it back to a blueprint.", + "action": "Take control" + }, "run": "[%key:ui::panel::config::automation::editor::actions::run%]", "rename": "[%key:ui::panel::config::automation::editor::triggers::rename%]", "show_trace": "Traces", @@ -3629,6 +3635,12 @@ "show_info": "[%key:ui::panel::config::automation::editor::show_info%]", "rename": "[%key:ui::panel::config::automation::editor::triggers::rename%]", "change_mode": "[%key:ui::panel::config::automation::editor::change_mode%]", + "take_control": "[%key:ui::panel::config::automation::editor::take_control%]", + "take_control_confirmation": { + "title": "Take control of script?", + "text": "This script is using a blueprint. By taking control, your script will be converted into a regular automation using actions. You will be able to edit it directly and you won't be able to convert it back to a blueprint.", + "action": "[%key:ui::panel::config::automation::editor::take_control_confirmation::action%]" + }, "read_only": "This script cannot be edited from the UI, because it is not stored in the ''scripts.yaml'' file.", "unavailable": "Script is unavailable", "migrate": "Migrate",