Add blueprint scripts (#9504)

This commit is contained in:
Bram Kragten 2021-10-26 18:32:40 +02:00 committed by GitHub
parent 54c64c15f3
commit 371804591d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 399 additions and 118 deletions

View File

@ -21,7 +21,9 @@ export interface ScriptEntity extends HassEntityBase {
}; };
} }
export interface ScriptConfig { export type ScriptConfig = ManualScriptConfig | BlueprintScriptConfig;
export interface ManualScriptConfig {
alias: string; alias: string;
sequence: Action | Action[]; sequence: Action | Action[];
icon?: string; icon?: string;
@ -29,7 +31,7 @@ export interface ScriptConfig {
max?: number; max?: number;
} }
export interface BlueprintScriptConfig extends ScriptConfig { export interface BlueprintScriptConfig extends ManualScriptConfig {
use_blueprint: { path: string; input?: BlueprintInput }; use_blueprint: { path: string; input?: BlueprintInput };
} }

View File

@ -29,6 +29,7 @@ import {
Blueprints, Blueprints,
deleteBlueprint, deleteBlueprint,
} from "../../../data/blueprint"; } from "../../../data/blueprint";
import { showScriptEditor } from "../../../data/script";
import { import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
@ -52,6 +53,12 @@ const createNewFunctions = {
use_blueprint: { path: blueprintMeta.path }, use_blueprint: { path: blueprintMeta.path },
}); });
}, },
script: (blueprintMeta: BlueprintMetaDataPath) => {
showScriptEditor({
alias: blueprintMeta.name,
use_blueprint: { path: blueprintMeta.path },
});
},
}; };
@customElement("ha-blueprint-overview") @customElement("ha-blueprint-overview")
@ -62,27 +69,38 @@ class HaBlueprintOverview extends LitElement {
@property({ type: Boolean }) public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@property() public route!: Route; @property({ attribute: false }) public route!: Route;
@property() public blueprints!: Blueprints; @property({ attribute: false }) public blueprints!: Record<
string,
Blueprints
>;
private _processedBlueprints = memoizeOne((blueprints: Blueprints) => { private _processedBlueprints = memoizeOne(
const result = Object.entries(blueprints).map(([path, blueprint]) => { (blueprints: Record<string, Blueprints>) => {
if ("error" in blueprint) { const result: any[] = [];
return { Object.entries(blueprints).forEach(([type, typeBlueprints]) =>
name: blueprint.error, Object.entries(typeBlueprints).forEach(([path, blueprint]) => {
error: true, if ("error" in blueprint) {
path, result.push({
}; name: blueprint.error,
} type,
return { error: true,
...blueprint.metadata, path,
error: false, });
path, } else {
}; result.push({
}); ...blueprint.metadata,
return result; type,
}); error: false,
path,
});
}
})
);
return result;
}
);
private _columns = memoizeOne( private _columns = memoizeOne(
(narrow, _language): DataTableColumnContainer => ({ (narrow, _language): DataTableColumnContainer => ({
@ -102,6 +120,20 @@ class HaBlueprintOverview extends LitElement {
` `
: undefined, : undefined,
}, },
type: {
title: this.hass.localize(
"ui.panel.config.blueprint.overview.headers.type"
),
template: (type: string) =>
html`${this.hass.localize(
`ui.panel.config.blueprint.overview.types.${type}`
)}`,
sortable: true,
filterable: true,
hidden: narrow,
direction: "asc",
width: "10%",
},
path: { path: {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.blueprint.overview.headers.file_name" "ui.panel.config.blueprint.overview.headers.file_name"
@ -114,25 +146,27 @@ class HaBlueprintOverview extends LitElement {
}, },
create: { create: {
title: "", title: "",
width: narrow ? undefined : "20%",
type: narrow ? "icon-button" : undefined, type: narrow ? "icon-button" : undefined,
template: (_, blueprint: any) => template: (_, blueprint: any) =>
blueprint.error blueprint.error
? "" ? ""
: narrow : narrow
? html` <ha-icon-button ? html`<ha-icon-button
.blueprint=${blueprint} .blueprint=${blueprint}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.blueprint.overview.use_blueprint" `ui.panel.config.blueprint.overview.create_${blueprint.domain}`
)} )}
.path=${mdiRobot}
@click=${this._createNew} @click=${this._createNew}
></ha-icon-button>` .path=${mdiRobot}
>
</ha-icon-button>`
: html`<mwc-button : html`<mwc-button
.blueprint=${blueprint} .blueprint=${blueprint}
@click=${this._createNew} @click=${this._createNew}
> >
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.blueprint.overview.use_blueprint" `ui.panel.config.blueprint.overview.create_${blueprint.domain}`
)} )}
</mwc-button>`, </mwc-button>`,
}, },

View File

@ -25,7 +25,7 @@ class HaConfigBlueprint extends HassRouterPage {
@property() public showAdvanced!: boolean; @property() public showAdvanced!: boolean;
@property() public blueprints: Blueprints = {}; @property() public blueprints: Record<string, Blueprints> = {};
protected routerOptions: RouterOptions = { protected routerOptions: RouterOptions = {
defaultPage: "dashboard", defaultPage: "dashboard",
@ -41,7 +41,11 @@ class HaConfigBlueprint extends HassRouterPage {
}; };
private async _getBlueprints() { private async _getBlueprints() {
this.blueprints = await fetchBlueprints(this.hass, "automation"); const [automation, script] = await Promise.all([
fetchBlueprints(this.hass, "automation"),
fetchBlueprints(this.hass, "script"),
]);
this.blueprints = { automation, script };
} }
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {

View File

@ -0,0 +1,205 @@
import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-blueprint-picker";
import "../../../components/ha-card";
import "../../../components/ha-circular-progress";
import "../../../components/ha-markdown";
import "../../../components/ha-selector/ha-selector";
import "../../../components/ha-settings-row";
import {
BlueprintOrError,
Blueprints,
fetchBlueprints,
} from "../../../data/blueprint";
import { BlueprintScriptConfig } from "../../../data/script";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
@customElement("blueprint-script-editor")
export class HaBlueprintScriptEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide!: boolean;
@property({ reflect: true, type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public config!: BlueprintScriptConfig;
@state() private _blueprints?: Blueprints;
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._getBlueprints();
}
private get _blueprint(): BlueprintOrError | undefined {
if (!this._blueprints) {
return undefined;
}
return this._blueprints[this.config.use_blueprint.path];
}
protected render() {
const blueprint = this._blueprint;
return html` <ha-config-section vertical .isWide=${this.isWide}>
<span slot="header"
>${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.header"
)}</span
>
<ha-card>
<div class="blueprint-picker-container">
${this._blueprints
? Object.keys(this._blueprints).length
? html`
<ha-blueprint-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.blueprint_to_use"
)}
.blueprints=${this._blueprints}
.value=${this.config.use_blueprint.path}
@value-changed=${this._blueprintChanged}
></ha-blueprint-picker>
`
: this.hass.localize(
"ui.panel.config.automation.editor.blueprint.no_blueprints"
)
: html`<ha-circular-progress active></ha-circular-progress>`}
</div>
${this.config.use_blueprint.path
? blueprint && "error" in blueprint
? html`<p class="warning padding">
There is an error in this Blueprint: ${blueprint.error}
</p>`
: html`${blueprint?.metadata.description
? html`<ha-markdown
class="card-content"
breaks
.content=${blueprint.metadata.description}
></ha-markdown>`
: ""}
${blueprint?.metadata?.input &&
Object.keys(blueprint.metadata.input).length
? Object.entries(blueprint.metadata.input).map(
([key, value]) =>
html`<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">${value?.name || key}</span>
<span slot="description">${value?.description}</span>
${value?.selector
? html`<ha-selector
.hass=${this.hass}
.selector=${value.selector}
.key=${key}
.value=${(this.config.use_blueprint.input &&
this.config.use_blueprint.input[key]) ??
value?.default}
@value-changed=${this._inputChanged}
></ha-selector>`
: html`<paper-input
.key=${key}
required
.value=${(this.config.use_blueprint.input &&
this.config.use_blueprint.input[key]) ??
value?.default}
@value-changed=${this._inputChanged}
no-label-float
></paper-input>`}
</ha-settings-row>`
)
: html`<p class="padding">
${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.no_inputs"
)}
</p>`}`
: ""}
</ha-card>
</ha-config-section>`;
}
private async _getBlueprints() {
this._blueprints = await fetchBlueprints(this.hass, "script");
}
private _blueprintChanged(ev) {
ev.stopPropagation();
if (this.config.use_blueprint.path === ev.detail.value) {
return;
}
fireEvent(this, "value-changed", {
value: {
...this.config,
use_blueprint: {
path: ev.detail.value,
},
},
});
}
private _inputChanged(ev) {
ev.stopPropagation();
const target = ev.target as any;
const key = target.key;
const value = ev.detail.value;
if (
(this.config.use_blueprint.input &&
this.config.use_blueprint.input[key] === value) ||
(!this.config.use_blueprint.input && value === "")
) {
return;
}
const input = { ...this.config.use_blueprint.input, [key]: value };
if (value === "" || value === undefined) {
delete input[key];
}
fireEvent(this, "value-changed", {
value: {
...this.config,
use_blueprint: {
...this.config.use_blueprint,
input,
},
},
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.padding {
padding: 16px;
}
.blueprint-picker-container {
padding: 16px;
}
p {
margin-bottom: 0;
}
ha-settings-row {
--paper-time-input-justify-content: flex-end;
border-top: 1px solid var(--divider-color);
}
:host(:not([narrow])) ha-settings-row paper-input {
width: 60%;
}
:host(:not([narrow])) ha-settings-row ha-selector {
width: 60%;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"blueprint-script-editor": HaBlueprintScriptEditor;
}
}

View File

@ -38,6 +38,7 @@ import {
Action, Action,
deleteScript, deleteScript,
getScriptEditorInitData, getScriptEditorInitData,
ManualScriptConfig,
MODES, MODES,
MODES_MAX, MODES_MAX,
ScriptConfig, ScriptConfig,
@ -55,6 +56,7 @@ import "../automation/action/ha-automation-action";
import { HaDeviceAction } from "../automation/action/types/ha-automation-action-device_id"; import { HaDeviceAction } from "../automation/action/types/ha-automation-action-device_id";
import "../ha-config-section"; import "../ha-config-section";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import "./blueprint-script-editor";
export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -236,60 +238,62 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
> >
</paper-input>` </paper-input>`
: ""} : ""}
<p> ${"use_blueprint" in this._config
${this.hass.localize( ? ""
"ui.panel.config.script.editor.modes.description", : html`<p>
"documentation_link", ${this.hass.localize(
html`<a "ui.panel.config.script.editor.modes.description",
href=${documentationUrl( "documentation_link",
this.hass, html`<a
"/integrations/script/#script-modes" href=${documentationUrl(
)} this.hass,
target="_blank" "/integrations/script/#script-modes"
rel="noreferrer" )}
>${this.hass.localize( target="_blank"
"ui.panel.config.script.editor.modes.documentation" rel="noreferrer"
)}</a >${this.hass.localize(
>` "ui.panel.config.script.editor.modes.documentation"
)} )}</a
</p> >`
<paper-dropdown-menu-light )}
.label=${this.hass.localize( </p>
"ui.panel.config.script.editor.modes.label" <paper-dropdown-menu-light
)} .label=${this.hass.localize(
no-animations "ui.panel.config.script.editor.modes.label"
> )}
<paper-listbox no-animations
slot="dropdown-content" >
.selected=${this._config.mode <paper-listbox
? MODES.indexOf(this._config.mode) slot="dropdown-content"
: 0} .selected=${this._config.mode
@iron-select=${this._modeChanged} ? MODES.indexOf(this._config.mode)
> : 0}
${MODES.map( @iron-select=${this._modeChanged}
(mode) => html` >
<paper-item .mode=${mode}> ${MODES.map(
${this.hass.localize( (mode) => html`
`ui.panel.config.script.editor.modes.${mode}` <paper-item .mode=${mode}>
) || mode} ${this.hass.localize(
</paper-item> `ui.panel.config.script.editor.modes.${mode}`
` ) || mode}
)} </paper-item>
</paper-listbox> `
</paper-dropdown-menu-light> )}
${this._config.mode && </paper-listbox>
MODES_MAX.includes(this._config.mode) </paper-dropdown-menu-light>
? html`<paper-input ${this._config.mode &&
.label=${this.hass.localize( MODES_MAX.includes(this._config.mode)
`ui.panel.config.script.editor.max.${this._config.mode}` ? html`<paper-input
)} .label=${this.hass.localize(
type="number" `ui.panel.config.script.editor.max.${this._config.mode}`
name="max" )}
.value=${this._config.max || "10"} type="number"
@value-changed=${this._valueChanged} name="max"
> .value=${this._config.max || "10"}
</paper-input>` @value-changed=${this._valueChanged}
: html``} >
</paper-input>`
: html``} `}
</div> </div>
${this.scriptEntityId ${this.scriptEntityId
? html` ? html`
@ -323,37 +327,48 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
</ha-card> </ha-card>
</ha-config-section> </ha-config-section>
<ha-config-section vertical .isWide=${this.isWide}> ${"use_blueprint" in this._config
<span slot="header"> ? html`<blueprint-script-editor
${this.hass.localize( .hass=${this.hass}
"ui.panel.config.script.editor.sequence" .narrow=${this.narrow}
)} .isWide=${this.isWide}
</span> .config=${this._config}
<span slot="introduction"> @value-changed=${this._configChanged}
<p> ></blueprint-script-editor>`
${this.hass.localize( : html`<ha-config-section
"ui.panel.config.script.editor.sequence_sentence" vertical
)} .isWide=${this.isWide}
</p>
<a
href=${documentationUrl(
this.hass,
"/docs/scripts/"
)}
target="_blank"
rel="noreferrer"
> >
${this.hass.localize( <span slot="header">
"ui.panel.config.script.editor.link_available_actions" ${this.hass.localize(
)} "ui.panel.config.script.editor.sequence"
</a> )}
</span> </span>
<ha-automation-action <span slot="introduction">
.actions=${this._config.sequence} <p>
@value-changed=${this._sequenceChanged} ${this.hass.localize(
.hass=${this.hass} "ui.panel.config.script.editor.sequence_sentence"
></ha-automation-action> )}
</ha-config-section> </p>
<a
href=${documentationUrl(
this.hass,
"/docs/scripts/"
)}
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.script.editor.link_available_actions"
)}
</a>
</span>
<ha-automation-action
.actions=${this._config.sequence}
@value-changed=${this._sequenceChanged}
.hass=${this.hass}
></ha-automation-action>
</ha-config-section>`}
` `
: ""} : ""}
</div> </div>
@ -427,7 +442,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
(!oldScript || oldScript !== this.scriptEntityId) (!oldScript || oldScript !== this.scriptEntityId)
) { ) {
this.hass this.hass
.callApi<ScriptConfig>( .callApi<ManualScriptConfig>(
"GET", "GET",
`config/script/config/${computeObjectId(this.scriptEntityId)}` `config/script/config/${computeObjectId(this.scriptEntityId)}`
) )
@ -466,11 +481,16 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
) { ) {
const initData = getScriptEditorInitData(); const initData = getScriptEditorInitData();
this._dirty = !!initData; this._dirty = !!initData;
this._config = { const baseConfig: Partial<ScriptConfig> = {
alias: this.hass.localize("ui.panel.config.script.editor.default_name"), alias: this.hass.localize("ui.panel.config.script.editor.default_name"),
sequence: [{ ...HaDeviceAction.defaultConfig }],
...initData,
}; };
if (!initData || !("use_blueprint" in initData)) {
baseConfig.sequence = [{ ...HaDeviceAction.defaultConfig }];
}
this._config = {
...baseConfig,
...initData,
} as ScriptConfig;
} }
} }
@ -548,6 +568,11 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
this._dirty = true; this._dirty = true;
} }
private _configChanged(ev) {
this._config = ev.detail.value;
this._dirty = true;
}
private _sequenceChanged(ev: CustomEvent): void { private _sequenceChanged(ev: CustomEvent): void {
this._config = { ...this._config!, sequence: ev.detail.value as Action[] }; this._config = { ...this._config!, sequence: ev.detail.value as Action[] };
this._errors = undefined; this._errors = undefined;
@ -749,3 +774,9 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
} }
customElements.define("ha-script-editor", HaScriptEditor); customElements.define("ha-script-editor", HaScriptEditor);
declare global {
interface HTMLElementTagNameMap {
"ha-script-editor": HaScriptEditor;
}
}

View File

@ -1794,13 +1794,18 @@
"learn_more": "Learn more about using blueprints", "learn_more": "Learn more about using blueprints",
"headers": { "headers": {
"name": "Name", "name": "Name",
"domain": "Domain", "type": "Type",
"file_name": "File name" "file_name": "File name"
}, },
"types": {
"automation": "Automation",
"script": "Script"
},
"confirm_delete_header": "Delete this blueprint?", "confirm_delete_header": "Delete this blueprint?",
"confirm_delete_text": "Are you sure you want to delete this blueprint?", "confirm_delete_text": "Are you sure you want to delete this blueprint?",
"add_blueprint": "Import blueprint", "add_blueprint": "Import blueprint",
"use_blueprint": "Create automation", "create_automation": "Create automation",
"create_script": "Create script",
"delete_blueprint": "Delete blueprint", "delete_blueprint": "Delete blueprint",
"share_blueprint": "Share blueprint", "share_blueprint": "Share blueprint",
"share_blueprint_no_url": "Unable to share blueprint: no source url", "share_blueprint_no_url": "Unable to share blueprint: no source url",