WIP initial Blueprint UI (#7695)

* WIP initial Blueprint UI

* Review comments

* localize
This commit is contained in:
Bram Kragten 2020-11-18 12:19:59 +01:00 committed by GitHub
parent 3aff4c96c4
commit 4973d8f629
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1645 additions and 301 deletions

View File

@ -0,0 +1,119 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { compare } from "../common/string/compare";
import { Blueprints, fetchBlueprints } from "../data/blueprint";
import { HomeAssistant } from "../types";
@customElement("ha-blueprint-picker")
class HaBluePrintPicker extends LitElement {
public hass?: HomeAssistant;
@property() public label?: string;
@property() public value = "";
@property() public domain = "automation";
@property() public blueprints?: Blueprints;
@property({ type: Boolean }) public disabled = false;
private _processedBlueprints = memoizeOne((blueprints?: Blueprints) => {
if (!blueprints) {
return [];
}
const result = Object.entries(blueprints).map(([path, blueprint]) => ({
...blueprint.metadata,
path,
}));
return result.sort((a, b) => compare(a.name, b.name));
});
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<paper-dropdown-menu-light
.label=${this.label ||
this.hass.localize("ui.components.blueprint-picker.label")}
.disabled=${this.disabled}
>
<paper-listbox
slot="dropdown-content"
.selected=${this.value}
attr-for-selected="data-blueprint-path"
@iron-select=${this._blueprintChanged}
>
<paper-item data-blueprint-path="">
${this.hass.localize(
"ui.components.blueprint-picker.select_blueprint"
)}
</paper-item>
${this._processedBlueprints(this.blueprints).map(
(blueprint) => html`
<paper-item data-blueprint-path=${blueprint.path}>
${blueprint.name}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
if (this.blueprints === undefined) {
fetchBlueprints(this.hass!, this.domain).then((blueprints) => {
this.blueprints = blueprints;
});
}
}
private _blueprintChanged(ev) {
const newValue = ev.detail.item.dataset.blueprintPath;
if (newValue !== this.value) {
this.value = ev.detail.value;
setTimeout(() => {
fireEvent(this, "value-changed", { value: newValue });
fireEvent(this, "change");
}, 0);
}
}
static get styles(): CSSResult {
return css`
:host {
display: inline-block;
}
paper-dropdown-menu-light {
width: 100%;
min-width: 200px;
display: block;
}
paper-listbox {
min-width: 200px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-blueprint-picker": HaBluePrintPicker;
}
}

View File

@ -4,6 +4,7 @@ import {
} from "home-assistant-js-websocket";
import { navigate } from "../common/navigate";
import { Context, HomeAssistant } from "../types";
import { BlueprintInput } from "./blueprint";
import { DeviceCondition, DeviceTrigger } from "./device_automation";
import { Action } from "./script";
@ -14,10 +15,14 @@ export interface AutomationEntity extends HassEntityBase {
};
}
export interface AutomationConfig {
export type AutomationConfig =
| ManualAutomationConfig
| BlueprintAutomationConfig;
export interface ManualAutomationConfig {
id?: string;
alias: string;
description: string;
alias?: string;
description?: string;
trigger: Trigger[];
condition?: Condition[];
action: Action[];
@ -25,6 +30,10 @@ export interface AutomationConfig {
max?: number;
}
export interface BlueprintAutomationConfig extends ManualAutomationConfig {
use_blueprint: { path: string; input?: BlueprintInput };
}
export interface ForDict {
hours?: number | string;
minutes?: number | string;

54
src/data/blueprint.ts Normal file
View File

@ -0,0 +1,54 @@
import { HomeAssistant } from "../types";
export type Blueprints = Record<string, Blueprint>;
export interface Blueprint {
metadata: BlueprintMetaData;
}
export interface BlueprintMetaData {
domain: string;
name: string;
input: BlueprintInput;
}
export type BlueprintInput = Record<string, any>;
export interface BlueprintImportResult {
url: string;
suggested_filename: string;
raw_data: string;
blueprint: Blueprint;
}
export const fetchBlueprints = (hass: HomeAssistant, domain: string) =>
hass.callWS<Blueprints>({ type: "blueprint/list", domain });
export const importBlueprint = (hass: HomeAssistant, url: string) =>
hass.callWS<BlueprintImportResult>({ type: "blueprint/import", url });
export const saveBlueprint = (
hass: HomeAssistant,
domain: string,
path: string,
yaml: string,
source_url?: string
) =>
hass.callWS({
type: "blueprint/save",
domain,
path,
yaml,
source_url,
});
export const deleteBlueprint = (
hass: HomeAssistant,
domain: string,
path: string
) =>
hass.callWS<BlueprintImportResult>({
type: "blueprint/delete",
domain,
path,
});

View File

@ -0,0 +1,306 @@
import {
css,
CSSResult,
customElement,
internalProperty,
LitElement,
property,
} from "lit-element";
import { html } from "lit-html";
import {
BlueprintAutomationConfig,
triggerAutomation,
} from "../../../data/automation";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import "../../../components/ha-card";
import "@polymer/paper-input/paper-textarea";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "../../../components/entity/ha-entity-toggle";
import "@material/mwc-button/mwc-button";
import "./trigger/ha-automation-trigger";
import "./condition/ha-automation-condition";
import "./action/ha-automation-action";
import { fireEvent } from "../../../common/dom/fire_event";
import { haStyle } from "../../../resources/styles";
import { HassEntity } from "home-assistant-js-websocket";
import { navigate } from "../../../common/navigate";
import {
Blueprint,
Blueprints,
fetchBlueprints,
} from "../../../data/blueprint";
import "../../../components/ha-blueprint-picker";
import "../../../components/ha-circular-progress";
@customElement("blueprint-automation-editor")
export class HaBlueprintAutomationEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide!: boolean;
@property() public narrow!: boolean;
@property() public config!: BlueprintAutomationConfig;
@property() public stateObj?: HassEntity;
@internalProperty() private _blueprints?: Blueprints;
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._getBlueprints();
}
private get _blueprint(): Blueprint | 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 .isWide=${this.isWide}>
${!this.narrow
? html` <span slot="header">${this.config.alias}</span> `
: ""}
<span slot="introduction">
${this.hass.localize(
"ui.panel.config.automation.editor.introduction"
)}
</span>
<ha-card>
<div class="card-content">
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.alias"
)}
name="alias"
.value=${this.config.alias}
@value-changed=${this._valueChanged}
>
</paper-input>
<paper-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.description.label"
)}
.placeholder=${this.hass.localize(
"ui.panel.config.automation.editor.description.placeholder"
)}
name="description"
.value=${this.config.description}
@value-changed=${this._valueChanged}
></paper-textarea>
</div>
${this.stateObj
? html`
<div class="card-actions layout horizontal justified center">
<div class="layout horizontal center">
<ha-entity-toggle
.hass=${this.hass}
.stateObj=${this.stateObj!}
></ha-entity-toggle>
${this.hass.localize(
"ui.panel.config.automation.editor.enable_disable"
)}
</div>
<mwc-button
@click=${this._excuteAutomation}
.stateObj=${this.stateObj}
>
${this.hass.localize("ui.card.automation.trigger")}
</mwc-button>
</div>
`
: ""}
</ha-card>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header"
>${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.header"
)}</span
>
<ha-card>
<div class="card-content">
<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>`}
<mwc-button @click=${this._navigateBlueprints}>
${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.manage_blueprints"
)}
</mwc-button>
</div>
${this.config.use_blueprint.path
? blueprint?.metadata?.input &&
Object.keys(blueprint.metadata.input).length
? html`<h3>
${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.inputs"
)}
</h3>
${Object.entries(blueprint.metadata.input).map(
([key, value]) =>
html`<div>
${value?.description}
<paper-input
.key=${key}
.label=${value?.name || key}
.value=${this.config.use_blueprint.input &&
this.config.use_blueprint.input[key]}
@value-changed=${this._inputChanged}
></paper-input>
</div>`
)}`
: this.hass.localize(
"ui.panel.config.automation.editor.blueprint.no_inputs"
)
: ""}
</div>
</ha-card>
</ha-config-section>`;
}
private async _getBlueprints() {
this._blueprints = await fetchBlueprints(this.hass, "automation");
}
private _excuteAutomation(ev: Event) {
triggerAutomation(this.hass, (ev.target as any).stateObj.entity_id);
}
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 = target.value;
if (
(this.config.use_blueprint.input &&
this.config.use_blueprint.input[key] === value) ||
(!this.config.use_blueprint.input && value === "")
) {
return;
}
fireEvent(this, "value-changed", {
value: {
...this.config!,
use_blueprint: {
...this.config.use_blueprint,
input: { ...this.config.use_blueprint.input, [key]: value },
},
},
});
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
const target = ev.target as any;
const name = target.name;
if (!name) {
return;
}
let newVal = ev.detail.value;
if (target.type === "number") {
newVal = Number(newVal);
}
if ((this.config![name] || "") === newVal) {
return;
}
fireEvent(this, "value-changed", {
value: { ...this.config!, [name]: newVal },
});
}
private _navigateBlueprints() {
navigate(this, "/config/blueprint");
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
ha-card {
overflow: hidden;
}
.errors {
padding: 20px;
font-weight: bold;
color: var(--error-color);
}
.content {
padding-bottom: 20px;
}
.blueprint-picker-container {
display: flex;
align-items: center;
justify-content: space-between;
}
h3 {
margin-top: 16px;
}
span[slot="introduction"] a {
color: var(--primary-color);
}
p {
margin-bottom: 0;
}
ha-entity-toggle {
margin-right: 8px;
}
mwc-fab {
position: relative;
bottom: calc(-80px - env(safe-area-inset-bottom));
transition: bottom 0.3s;
}
mwc-fab.dirty {
bottom: 0;
}
.selected_menu_item {
color: var(--primary-color);
}
li[role="separator"] {
border-bottom-color: var(--divider-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"blueprint-automation-editor": HaBlueprintAutomationEditor;
}
}

View File

@ -0,0 +1,175 @@
import "@material/mwc-button";
import "../../../components/ha-circular-progress";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import "../../../components/ha-dialog";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import {
AutomationConfig,
showAutomationEditor,
} from "../../../data/automation";
import { showThingtalkDialog } from "./thingtalk/show-dialog-thingtalk";
import "../../../components/ha-card";
import "../../../components/ha-blueprint-picker";
@customElement("ha-dialog-new-automation")
class DialogNewAutomation extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _opened = false;
public showDialog(): void {
this._opened = true;
}
public closeDialog(): void {
this._opened = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._opened) {
return html``;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${this.hass.localize(
"ui.panel.config.automation.dialog_new.header"
)}
>
<div>
${this.hass.localize("ui.panel.config.automation.dialog_new.how")}
<div class="container">
${isComponentLoaded(this.hass, "cloud")
? html`<ha-card outlined>
<div>
<h3>
${this.hass.localize(
"ui.panel.config.automation.dialog_new.thingtalk.header"
)}
</h3>
${this.hass.localize(
"ui.panel.config.automation.dialog_new.thingtalk.intro"
)}
<div class="side-by-side">
<paper-input
id="input"
.label=${this.hass.localize(
"ui.panel.config.automation.dialog_new.thingtalk.input_label"
)}
></paper-input>
<mwc-button @click=${this._thingTalk}
>${this.hass.localize(
"ui.panel.config.automation.dialog_new.thingtalk.create"
)}</mwc-button
>
</div>
</div>
</ha-card>`
: html``}
${isComponentLoaded(this.hass, "blueprint")
? html`<ha-card outlined>
<div>
<h3>
${this.hass.localize(
"ui.panel.config.automation.dialog_new.blueprint.use_blueprint"
)}
</h3>
<ha-blueprint-picker
@value-changed=${this._blueprintPicked}
.hass=${this.hass}
></ha-blueprint-picker>
</div>
</ha-card>`
: html``}
</div>
</div>
<mwc-button slot="primaryAction" @click=${this._blank}>
${this.hass.localize(
"ui.panel.config.automation.dialog_new.start_empty"
)}
</mwc-button>
</ha-dialog>
`;
}
private _thingTalk() {
this.closeDialog();
showThingtalkDialog(this, {
callback: (config: Partial<AutomationConfig> | undefined) =>
showAutomationEditor(this, config),
input: this.shadowRoot!.querySelector("paper-input")!.value as string,
});
}
private _blueprintPicked(ev: CustomEvent) {
showAutomationEditor(this, { use_blueprint: { path: ev.detail.value } });
this.closeDialog();
}
private _blank() {
showAutomationEditor(this);
this.closeDialog();
}
static get styles(): CSSResult[] {
return [
haStyle,
haStyleDialog,
css`
.container {
display: flex;
}
ha-card {
width: calc(50% - 8px);
margin: 4px;
}
ha-card div {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
ha-card {
box-sizing: border-box;
padding: 8px;
}
ha-blueprint-picker {
width: 100%;
}
.side-by-side {
display: flex;
flex-direction: row;
align-items: flex-end;
}
@media all and (max-width: 500px) {
.container {
flex-direction: column;
}
ha-card {
width: 100%;
}
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-dialog-new-automation": DialogNewAutomation;
}
}

View File

@ -12,7 +12,6 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@polymer/paper-input/paper-textarea";
import "@material/mwc-list/mwc-list-item";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import { PaperListboxElement } from "@polymer/paper-listbox";
import {
css,
CSSResult,
@ -36,14 +35,11 @@ import type { HaYamlEditor } from "../../../components/ha-yaml-editor";
import {
AutomationConfig,
AutomationEntity,
Condition,
deleteAutomation,
getAutomationEditorInitData,
showAutomationEditor,
Trigger,
triggerAutomation,
} from "../../../data/automation";
import { Action } from "../../../data/script";
import {
showAlertDialog,
showConfirmationDialog,
@ -53,7 +49,6 @@ import "../../../layouts/hass-tabs-subpage";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
import "./action/ha-automation-action";
@ -61,11 +56,13 @@ import { HaDeviceAction } from "./action/types/ha-automation-action-device_id";
import "./condition/ha-automation-condition";
import "./trigger/ha-automation-trigger";
import { HaDeviceTrigger } from "./trigger/types/ha-automation-trigger-device";
const MODES = ["single", "restart", "queued", "parallel"];
const MODES_MAX = ["queued", "parallel"];
import "./manual-automation-editor";
import "./blueprint-automation-editor";
declare global {
interface HTMLElementTagNameMap {
"ha-automation-editor": HaAutomationEditor;
}
// for fire event
interface HASSDomEvents {
"ui-mode-not-available": Error;
@ -193,6 +190,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
</ha-svg-icon>
</mwc-list-item>
</ha-button-menu>
${this._config
? html`
${this.narrow
@ -204,217 +202,19 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
: ""}
${this._mode === "gui"
? html`
<ha-config-section .isWide=${this.isWide}>
${!this.narrow
? html`
<span slot="header">${this._config.alias}</span>
`
: ""}
<span slot="introduction">
${this.hass.localize(
"ui.panel.config.automation.editor.introduction"
)}
</span>
<ha-card>
<div class="card-content">
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.alias"
)}
name="alias"
.value=${this._config.alias}
@value-changed=${this._valueChanged}
>
</paper-input>
<paper-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.description.label"
)}
.placeholder=${this.hass.localize(
"ui.panel.config.automation.editor.description.placeholder"
)}
name="description"
.value=${this._config.description}
@value-changed=${this._valueChanged}
></paper-textarea>
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.modes.description",
"documentation_link",
html`<a
href="${documentationUrl(
this.hass,
"/integrations/automation/#automation-modes"
)}"
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.automation.editor.modes.documentation"
)}</a
>`
)}
</p>
<paper-dropdown-menu-light
.label=${this.hass.localize(
"ui.panel.config.automation.editor.modes.label"
)}
no-animations
>
<paper-listbox
slot="dropdown-content"
.selected=${this._config.mode
? MODES.indexOf(this._config.mode)
: 0}
@iron-select=${this._modeChanged}
>
${MODES.map(
(mode) => html`
<paper-item .mode=${mode}>
${this.hass.localize(
`ui.panel.config.automation.editor.modes.${mode}`
) || mode}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
${this._config.mode &&
MODES_MAX.includes(this._config.mode)
? html`<paper-input
.label=${this.hass.localize(
`ui.panel.config.automation.editor.max.${this._config.mode}`
)}
type="number"
name="max"
.value=${this._config.max || "10"}
@value-changed=${this._valueChanged}
>
</paper-input>`
: html``}
</div>
${stateObj
? html`
<div
class="card-actions layout horizontal justified center"
>
<div class="layout horizontal center">
<ha-entity-toggle
${"use_blueprint" in this._config
? html`<blueprint-automation-editor
.hass=${this.hass}
.stateObj=${stateObj}
></ha-entity-toggle>
${this.hass.localize(
"ui.panel.config.automation.editor.enable_disable"
)}
</div>
<mwc-button
@click=${this._excuteAutomation}
.config=${this._config}
@value-changed=${this._valueChanged}
></blueprint-automation-editor>`
: html`<manual-automation-editor
.hass=${this.hass}
.stateObj=${stateObj}
>
${this.hass.localize(
"ui.card.automation.trigger"
)}
</mwc-button>
</div>
`
: ""}
</ha-card>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.introduction"
)}
</p>
<a
href="${documentationUrl(
this.hass,
"/docs/automation/trigger/"
)}"
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.learn_more"
)}
</a>
</span>
<ha-automation-trigger
.triggers=${this._config.trigger}
@value-changed=${this._triggerChanged}
.hass=${this.hass}
></ha-automation-trigger>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.introduction"
)}
</p>
<a
href="${documentationUrl(
this.hass,
"/docs/scripts/conditions/"
)}"
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.learn_more"
)}
</a>
</span>
<ha-automation-condition
.conditions=${this._config.condition || []}
@value-changed=${this._conditionChanged}
.hass=${this.hass}
></ha-automation-condition>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.introduction"
)}
</p>
<a
href="${documentationUrl(
this.hass,
"/docs/automation/action/"
)}"
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.learn_more"
)}
</a>
</span>
<ha-automation-action
.actions=${this._config.action}
@value-changed=${this._actionChanged}
.hass=${this.hass}
></ha-automation-action>
</ha-config-section>
.config=${this._config}
@value-changed=${this._valueChanged}
></manual-automation-editor>`}
`
: this._mode === "yaml"
? html`
@ -531,18 +331,26 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
if (changedProps.has("automationId") && !this.automationId && this.hass) {
const initData = getAutomationEditorInitData();
this._dirty = !!initData;
this._config = {
let baseConfig: Partial<AutomationConfig> = {
alias: this.hass.localize(
"ui.panel.config.automation.editor.default_name"
),
description: "",
};
if (!initData || !("use_blueprint" in initData)) {
baseConfig = {
...baseConfig,
mode: "single",
trigger: [{ platform: "device", ...HaDeviceTrigger.defaultConfig }],
condition: [],
action: [{ ...HaDeviceAction.defaultConfig }],
...initData,
};
}
this._config = {
...baseConfig,
...initData,
} as AutomationConfig;
}
if (
changedProps.has("automations") &&
@ -560,58 +368,11 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
this._entityId = automation?.entity_id;
}
private _modeChanged(ev: CustomEvent) {
const mode = ((ev.target as PaperListboxElement)?.selectedItem as any)
?.mode;
if (mode === this._config!.mode) {
return;
}
this._config = { ...this._config!, mode };
if (!MODES_MAX.includes(mode)) {
delete this._config.max;
}
this._dirty = true;
}
private _valueChanged(ev: CustomEvent) {
private _valueChanged(ev: CustomEvent<{ value: AutomationConfig }>) {
ev.stopPropagation();
const target = ev.target as any;
const name = target.name;
if (!name) {
return;
}
let newVal = ev.detail.value;
if (target.type === "number") {
newVal = Number(newVal);
}
if ((this._config![name] || "") === newVal) {
return;
}
this._config = { ...this._config!, [name]: newVal };
this._config = ev.detail.value;
this._dirty = true;
}
private _triggerChanged(ev: CustomEvent): void {
this._config = { ...this._config!, trigger: ev.detail.value as Trigger[] };
this._errors = undefined;
this._dirty = true;
}
private _conditionChanged(ev: CustomEvent): void {
this._config = {
...this._config!,
condition: ev.detail.value as Condition[],
};
this._errors = undefined;
this._dirty = true;
}
private _actionChanged(ev: CustomEvent): void {
this._config = { ...this._config!, action: ev.detail.value as Action[] };
this._errors = undefined;
this._dirty = true;
}
private _excuteAutomation(ev: Event) {

View File

@ -12,7 +12,6 @@ import {
} from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { formatDateTime } from "../../../common/datetime/format_date_time";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
@ -20,19 +19,16 @@ import { DataTableColumnContainer } from "../../../components/data-table/ha-data
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../components/entity/ha-entity-toggle";
import "../../../components/ha-svg-icon";
import {
AutomationConfig,
AutomationEntity,
showAutomationEditor,
triggerAutomation,
} from "../../../data/automation";
import { AutomationEntity, triggerAutomation } from "../../../data/automation";
import { UNAVAILABLE_STATES } from "../../../data/entity";
import "../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import { showThingtalkDialog } from "./show-dialog-thingtalk";
import { documentationUrl } from "../../../util/documentation-url";
import { showNewAutomationDialog } from "./show-dialog-new-automation";
import { navigate } from "../../../common/navigate";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
@customElement("ha-automation-picker")
class HaAutomationPicker extends LitElement {
@ -220,14 +216,14 @@ class HaAutomationPicker extends LitElement {
}
private _createNew() {
if (!isComponentLoaded(this.hass, "cloud")) {
showAutomationEditor(this);
return;
if (
isComponentLoaded(this.hass, "cloud") ||
isComponentLoaded(this.hass, "blueprint")
) {
showNewAutomationDialog(this);
} else {
navigate(this, "/config/automation/edit/new");
}
showThingtalkDialog(this, {
callback: (config: Partial<AutomationConfig> | undefined) =>
showAutomationEditor(this, config),
});
}
static get styles(): CSSResult {

View File

@ -0,0 +1,349 @@
import {
css,
CSSResult,
customElement,
LitElement,
property,
} from "lit-element";
import { html } from "lit-html";
import {
Condition,
ManualAutomationConfig,
Trigger,
triggerAutomation,
} from "../../../data/automation";
import { Action, MODES, MODES_MAX } from "../../../data/script";
import { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import "../ha-config-section";
import "../../../components/ha-card";
import "@polymer/paper-input/paper-textarea";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "../../../components/entity/ha-entity-toggle";
import "@material/mwc-button/mwc-button";
import "./trigger/ha-automation-trigger";
import "./condition/ha-automation-condition";
import "./action/ha-automation-action";
import { fireEvent } from "../../../common/dom/fire_event";
import { PaperListboxElement } from "@polymer/paper-listbox";
import { haStyle } from "../../../resources/styles";
import { HassEntity } from "home-assistant-js-websocket";
@customElement("manual-automation-editor")
export class HaManualAutomationEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide!: boolean;
@property() public narrow!: boolean;
@property() public config!: ManualAutomationConfig;
@property() public stateObj?: HassEntity;
protected render() {
return html`<ha-config-section .isWide=${this.isWide}>
${!this.narrow
? html` <span slot="header">${this.config.alias}</span> `
: ""}
<span slot="introduction">
${this.hass.localize(
"ui.panel.config.automation.editor.introduction"
)}
</span>
<ha-card>
<div class="card-content">
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.alias"
)}
name="alias"
.value=${this.config.alias}
@value-changed=${this._valueChanged}
>
</paper-input>
<paper-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.description.label"
)}
.placeholder=${this.hass.localize(
"ui.panel.config.automation.editor.description.placeholder"
)}
name="description"
.value=${this.config.description}
@value-changed=${this._valueChanged}
></paper-textarea>
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.modes.description",
"documentation_link",
html`<a
href="${documentationUrl(
this.hass,
"/integrations/automation/#automation-modes"
)}"
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.automation.editor.modes.documentation"
)}</a
>`
)}
</p>
<paper-dropdown-menu-light
.label=${this.hass.localize(
"ui.panel.config.automation.editor.modes.label"
)}
no-animations
>
<paper-listbox
slot="dropdown-content"
.selected=${this.config.mode
? MODES.indexOf(this.config.mode)
: 0}
@iron-select=${this._modeChanged}
>
${MODES.map(
(mode) => html`
<paper-item .mode=${mode}>
${this.hass.localize(
`ui.panel.config.automation.editor.modes.${mode}`
) || mode}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
${this.config.mode && MODES_MAX.includes(this.config.mode)
? html`<paper-input
.label=${this.hass.localize(
`ui.panel.config.automation.editor.max.${this.config.mode}`
)}
type="number"
name="max"
.value=${this.config.max || "10"}
@value-changed=${this._valueChanged}
>
</paper-input>`
: html``}
</div>
${this.stateObj
? html`
<div class="card-actions layout horizontal justified center">
<div class="layout horizontal center">
<ha-entity-toggle
.hass=${this.hass}
.stateObj=${this.stateObj!}
></ha-entity-toggle>
${this.hass.localize(
"ui.panel.config.automation.editor.enable_disable"
)}
</div>
<mwc-button
@click=${this._excuteAutomation}
.stateObj=${this.stateObj}
>
${this.hass.localize("ui.card.automation.trigger")}
</mwc-button>
</div>
`
: ""}
</ha-card>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.introduction"
)}
</p>
<a
href="${documentationUrl(this.hass, "/docs/automation/trigger/")}"
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.learn_more"
)}
</a>
</span>
<ha-automation-trigger
.triggers=${this.config.trigger}
@value-changed=${this._triggerChanged}
.hass=${this.hass}
></ha-automation-trigger>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.introduction"
)}
</p>
<a
href="${documentationUrl(this.hass, "/docs/scripts/conditions/")}"
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.learn_more"
)}
</a>
</span>
<ha-automation-condition
.conditions=${this.config.condition || []}
@value-changed=${this._conditionChanged}
.hass=${this.hass}
></ha-automation-condition>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.introduction"
)}
</p>
<a
href="${documentationUrl(this.hass, "/docs/automation/action/")}"
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.learn_more"
)}
</a>
</span>
<ha-automation-action
.actions=${this.config.action}
@value-changed=${this._actionChanged}
.hass=${this.hass}
></ha-automation-action>
</ha-config-section>`;
}
private _excuteAutomation(ev: Event) {
triggerAutomation(this.hass, (ev.target as any).stateObj.entity_id);
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
const target = ev.target as any;
const name = target.name;
if (!name) {
return;
}
let newVal = ev.detail.value;
if (target.type === "number") {
newVal = Number(newVal);
}
if ((this.config![name] || "") === newVal) {
return;
}
fireEvent(this, "value-changed", {
value: { ...this.config!, [name]: newVal },
});
}
private _modeChanged(ev: CustomEvent) {
const mode = ((ev.target as PaperListboxElement)?.selectedItem as any)
?.mode;
if (mode === this.config!.mode) {
return;
}
fireEvent(this, "value-changed", {
value: {
...this.config!,
mode,
max: !MODES_MAX.includes(mode) ? undefined : this.config.max,
},
});
}
private _triggerChanged(ev: CustomEvent): void {
fireEvent(this, "value-changed", {
value: { ...this.config!, trigger: ev.detail.value as Trigger[] },
});
}
private _conditionChanged(ev: CustomEvent): void {
fireEvent(this, "value-changed", {
value: {
...this.config!,
condition: ev.detail.value as Condition[],
},
});
}
private _actionChanged(ev: CustomEvent): void {
fireEvent(this, "value-changed", {
value: { ...this.config!, action: ev.detail.value as Action[] },
});
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
ha-card {
overflow: hidden;
}
.errors {
padding: 20px;
font-weight: bold;
color: var(--error-color);
}
.content {
padding-bottom: 20px;
}
span[slot="introduction"] a {
color: var(--primary-color);
}
p {
margin-bottom: 0;
}
ha-entity-toggle {
margin-right: 8px;
}
mwc-fab {
position: relative;
bottom: calc(-80px - env(safe-area-inset-bottom));
transition: bottom 0.3s;
}
mwc-fab.dirty {
bottom: 0;
}
.selected_menu_item {
color: var(--primary-color);
}
li[role="separator"] {
border-bottom-color: var(--divider-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"manual-automation-editor": HaManualAutomationEditor;
}
}

View File

@ -0,0 +1,12 @@
import { fireEvent } from "../../../common/dom/fire_event";
export const loadNewAutomationDialog = () =>
import(/* webpackChunkName: "thingtalk-dialog" */ "./dialog-new-automation");
export const showNewAutomationDialog = (element: HTMLElement): void => {
fireEvent(element, "show-dialog", {
dialogTag: "ha-dialog-new-automation",
dialogImport: loadNewAutomationDialog,
dialogParams: {},
});
};

View File

@ -20,7 +20,7 @@ import { convertThingTalk } from "../../../../data/cloud";
import type { PolymerChangedEvent } from "../../../../polymer-types";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type { ThingtalkDialogParams } from "../show-dialog-thingtalk";
import type { ThingtalkDialogParams } from "./show-dialog-thingtalk";
import "./ha-thingtalk-placeholders";
import type { PlaceholderValues } from "./ha-thingtalk-placeholders";
@ -50,16 +50,21 @@ class DialogThingtalk extends LitElement {
@internalProperty() private _placeholders?: PlaceholderContainer;
@query("#input", true) private _input?: PaperInputElement;
@query("#input") private _input?: PaperInputElement;
private _value!: string;
private _value?: string;
private _config!: Partial<AutomationConfig>;
public showDialog(params: ThingtalkDialogParams): void {
public async showDialog(params: ThingtalkDialogParams): Promise<void> {
this._params = params;
this._error = undefined;
this._opened = true;
if (params.input) {
this._value = params.input;
await this.updateComplete;
this._generate();
}
}
protected render(): TemplateResult {
@ -126,6 +131,7 @@ class DialogThingtalk extends LitElement {
<paper-input
id="input"
label="What should this automation do?"
.value=${this._value}
autofocus
@keyup=${this._handleKeyUp}
></paper-input>

View File

@ -1,14 +1,13 @@
import { fireEvent } from "../../../common/dom/fire_event";
import { AutomationConfig } from "../../../data/automation";
import { fireEvent } from "../../../../common/dom/fire_event";
import { AutomationConfig } from "../../../../data/automation";
export interface ThingtalkDialogParams {
callback: (config: Partial<AutomationConfig> | undefined) => void;
input?: string;
}
export const loadThingtalkDialog = () =>
import(
/* webpackChunkName: "thingtalk-dialog" */ "./thingtalk/dialog-thingtalk"
);
import(/* webpackChunkName: "thingtalk-dialog" */ "./dialog-thingtalk");
export const showThingtalkDialog = (
element: HTMLElement,

View File

@ -0,0 +1,186 @@
import "@material/mwc-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import "../../../components/ha-circular-progress";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
internalProperty,
query,
TemplateResult,
} from "lit-element";
import "../../../components/ha-dialog";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import {
BlueprintImportResult,
importBlueprint,
saveBlueprint,
} from "../../../data/blueprint";
@customElement("ha-dialog-import-blueprint")
class DialogImportBlueprint extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _params?;
@internalProperty() private _importing = false;
@internalProperty() private _saving = false;
@internalProperty() private _error?: string;
@internalProperty() private _result?: BlueprintImportResult;
@query("#input") private _input?: PaperInputElement;
public showDialog(params): void {
this._params = params;
this._error = undefined;
}
public closeDialog(): void {
this._error = undefined;
this._result = undefined;
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._params) {
return html``;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${this.hass.localize("ui.panel.config.blueprint.add.header")}
>
<div>
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
${this._result
? html`${this.hass.localize(
"ui.panel.config.blueprint.add.import",
"name",
"domain",
html`<b>${this._result.blueprint.metadata.name}</b>`,
this._result.blueprint.metadata.domain
)}
<paper-input
id="input"
.value=${this._result.suggested_filename}
label="Filename"
></paper-input>
<pre>${this._result.raw_data}</pre>`
: html`${this.hass.localize(
"ui.panel.config.blueprint.add.import_introduction"
)}<paper-input
id="input"
.label=${this.hass.localize(
"ui.panel.config.blueprint.add.url"
)}
dialogInitialFocus
></paper-input>`}
</div>
${!this._result
? html`<mwc-button
slot="primaryAction"
@click=${this._import}
.disabled=${this._importing}
>
${this._importing
? html`<ha-circular-progress
active
size="small"
.title=${this.hass.localize(
"ui.panel.config.blueprint.add.importing"
)}
></ha-circular-progress>`
: ""}
${this.hass.localize("ui.panel.config.blueprint.add.import_btn")}
</mwc-button>`
: html`<mwc-button
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._saving}
>
${this.hass.localize("ui.common.cancel")}
</mwc-button>
<mwc-button
slot="primaryAction"
@click=${this._save}
.disabled=${this._saving}
>
${this._saving
? html`<ha-circular-progress
active
size="small"
.title=${this.hass.localize(
"ui.panel.config.blueprint.add.saving"
)}
></ha-circular-progress>`
: ""}
${this.hass.localize("ui.panel.config.blueprint.add.save_btn")}
</mwc-button>`}
</ha-dialog>
`;
}
private async _import() {
this._importing = true;
this._error = undefined;
try {
const url = this._input?.value;
if (!url) {
this._error = this.hass.localize(
"ui.panel.config.blueprint.add.error_no_url"
);
return;
}
this._result = await importBlueprint(this.hass, url);
} catch (e) {
this._error = e.message;
} finally {
this._importing = false;
}
}
private async _save() {
this._saving = true;
try {
const filename = this._input?.value;
if (!filename) {
return;
}
await saveBlueprint(
this.hass,
this._result!.blueprint.metadata.domain,
filename,
this._result!.raw_data,
this._result!.url
);
this._params.importedCallback();
this.closeDialog();
} catch (e) {
this._error = e.message;
} finally {
this._saving = false;
}
}
static get styles(): CSSResult[] {
return [haStyleDialog, css``];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-dialog-import-blueprint": DialogImportBlueprint;
}
}

View File

@ -0,0 +1,212 @@
import "@material/mwc-fab";
import "@material/mwc-icon-button";
import { mdiPlus, mdiHelpCircle, mdiDelete } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import {
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import "../../../components/entity/ha-entity-toggle";
import "../../../components/ha-svg-icon";
import "../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import { documentationUrl } from "../../../util/documentation-url";
import {
BlueprintMetaData,
Blueprints,
deleteBlueprint,
} from "../../../data/blueprint";
import { showAddBlueprintDialog } from "./show-dialog-import-blueprint";
import { showAutomationEditor } from "../../../data/automation";
import { fireEvent } from "../../../common/dom/fire_event";
interface BlueprintMetaDataPath extends BlueprintMetaData {
path: string;
}
const createNewFunctions = {
automation: (
context: HaBlueprintOverview,
blueprintMeta: BlueprintMetaDataPath
) => {
showAutomationEditor(context, {
alias: blueprintMeta.name,
use_blueprint: { path: blueprintMeta.path },
});
},
};
@customElement("ha-blueprint-overview")
class HaBlueprintOverview extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide!: boolean;
@property({ type: Boolean }) public narrow!: boolean;
@property() public route!: Route;
@property() public blueprints!: Blueprints;
private _processedBlueprints = memoizeOne((blueprints: Blueprints) => {
const result = Object.entries(blueprints).map(([path, blueprint]) => ({
...blueprint.metadata,
path,
}));
return result;
});
private _columns = memoizeOne(
(_language): DataTableColumnContainer => {
const columns: DataTableColumnContainer = {
name: {
title: this.hass.localize(
"ui.panel.config.blueprint.overview.headers.name"
),
sortable: true,
filterable: true,
direction: "asc",
grows: true,
},
};
columns.domain = {
title: "Domain",
sortable: true,
filterable: true,
direction: "asc",
width: "20%",
};
columns.path = {
title: "Path",
sortable: true,
filterable: true,
direction: "asc",
width: "20%",
};
columns.create = {
title: "",
type: "icon-button",
template: (_, blueprint) => html`<mwc-icon-button
.blueprint=${blueprint}
@click=${(ev) => this._createNew(ev)}
><ha-svg-icon .path=${mdiPlus}></ha-svg-icon
></mwc-icon-button>`,
};
columns.delete = {
title: "",
type: "icon-button",
template: (_, blueprint) => html`<mwc-icon-button
.blueprint=${blueprint}
@click=${(ev) => this._delete(ev)}
><ha-svg-icon .path=${mdiDelete}></ha-svg-icon
></mwc-icon-button>`,
};
return columns;
}
);
protected render(): TemplateResult {
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.automation}
.columns=${this._columns(this.hass.language)}
.data=${this._processedBlueprints(this.blueprints)}
id="entity_id"
.noDataText=${this.hass.localize(
"ui.panel.config.blueprint.overview.no_blueprints"
)}
hasFab
>
<mwc-icon-button slot="toolbar-icon" @click=${this._showHelp}>
<ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon>
</mwc-icon-button>
<mwc-fab
slot="fab"
title=${this.hass.localize(
"ui.panel.config.blueprint.overview.add_blueprint"
)}
@click=${this._addBlueprint}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
</hass-tabs-subpage-data-table>
`;
}
private _showHelp() {
showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.blueprint.caption"),
text: html`
${this.hass.localize("ui.panel.config.blueprint.overview.introduction")}
<p>
<a
href="${documentationUrl(this.hass, "/docs/blueprint/editor/")}"
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.blueprint.overview.learn_more"
)}
</a>
</p>
`,
});
}
private _addBlueprint() {
showAddBlueprintDialog(this, { importedCallback: () => this._reload() });
}
private _reload() {
fireEvent(this, "reload-blueprints");
}
private _createNew(ev) {
const blueprint = ev.currentTarget.blueprint as BlueprintMetaDataPath;
createNewFunctions[blueprint.domain](this, blueprint);
}
private async _delete(ev) {
const blueprint = ev.currentTarget.blueprint;
if (
!(await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.blueprint.overview.confirm_delete_header"
),
text: this.hass.localize(
"ui.panel.config.blueprint.overview.confirm_delete_text"
),
}))
) {
return;
}
await deleteBlueprint(this.hass, blueprint.domain, blueprint.path);
fireEvent(this, "reload-blueprints");
}
static get styles(): CSSResult {
return haStyle;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-blueprint-overview": HaBlueprintOverview;
}
}

View File

@ -0,0 +1,76 @@
import { customElement, property, PropertyValues } from "lit-element";
import {
HassRouterPage,
RouterOptions,
} from "../../../layouts/hass-router-page";
import "./ha-blueprint-overview";
import { HomeAssistant } from "../../../types";
import { Blueprints, fetchBlueprints } from "../../../data/blueprint";
declare global {
// for fire event
interface HASSDomEvents {
"reload-blueprints": undefined;
}
}
@customElement("ha-config-blueprint")
class HaConfigBlueprint extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public narrow!: boolean;
@property() public isWide!: boolean;
@property() public showAdvanced!: boolean;
@property() public blueprints: Blueprints = {};
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
routes: {
dashboard: {
tag: "ha-blueprint-overview",
cache: true,
},
edit: {
tag: "ha-blueprint-editor",
},
},
};
private async _getBlueprints() {
this.blueprints = await fetchBlueprints(this.hass, "automation");
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this.addEventListener("reload-blueprints", () => {
this._getBlueprints();
});
this._getBlueprints();
}
protected updatePageEl(pageEl, changedProps: PropertyValues) {
pageEl.hass = this.hass;
pageEl.narrow = this.narrow;
pageEl.isWide = this.isWide;
pageEl.route = this.routeTail;
pageEl.showAdvanced = this.showAdvanced;
pageEl.blueprints = this.blueprints;
if (
(!changedProps || changedProps.has("route")) &&
this._currentPage === "edit"
) {
const blueprintId = this.routeTail.path.substr(1);
pageEl.blueprintId = blueprintId === "new" ? null : blueprintId;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-blueprint": HaConfigBlueprint;
}
}

View File

@ -0,0 +1,17 @@
import { fireEvent } from "../../../common/dom/fire_event";
export const loadImportBlueprintDialog = () =>
import(
/* webpackChunkName: "add-blueprint-dialog" */ "./dialog-import-blueprint"
);
export const showAddBlueprintDialog = (
element: HTMLElement,
dialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "ha-dialog-import-blueprint",
dialogImport: loadImportBlueprintDialog,
dialogParams,
});
};

View File

@ -33,6 +33,7 @@ import {
mdiMathLog,
mdiPencil,
mdiNfcVariant,
mdiPaletteSwatch,
} from "@mdi/js";
declare global {
@ -74,6 +75,12 @@ export const configSections: { [name: string]: PageNavigation[] } = {
},
],
automation: [
{
component: "blueprint",
path: "/config/blueprint",
translationKey: "ui.panel.config.blueprint.caption",
iconPath: mdiPaletteSwatch,
},
{
component: "automation",
path: "/config/automation",
@ -92,6 +99,8 @@ export const configSections: { [name: string]: PageNavigation[] } = {
translationKey: "ui.panel.config.script.caption",
iconPath: mdiScriptText,
},
],
helpers: [
{
component: "helpers",
path: "/config/helpers",
@ -206,6 +215,13 @@ class HaPanelConfig extends HassRouterPage {
/* webpackChunkName: "panel-config-automation" */ "./automation/ha-config-automation"
),
},
blueprint: {
tag: "ha-config-blueprint",
load: () =>
import(
/* webpackChunkName: "panel-config-blueprint" */ "./blueprint/ha-config-blueprint"
),
},
tags: {
tag: "ha-config-tags",
load: () =>

View File

@ -339,6 +339,11 @@
"add_user": "Add user",
"remove_user": "Remove user"
},
"blueprint-picker": {
"select_blueprint": "Select a Blueprint",
"add_user": "Add user",
"remove_user": "Remove user"
},
"device-picker": {
"clear": "Clear",
"toggle": "Toggle",
@ -1110,6 +1115,19 @@
"name": "Name"
}
},
"dialog_new": {
"header": "Create a new automation",
"how": "How do you want to create your new automation?",
"blueprint": { "use_blueprint": "Use a blueprint" },
"thingtalk": {
"header": "Describe the automation you want to create",
"intro": "And we will try to create it for you. For example: Turn the lights off when I leave.",
"input_label": "What should this automation do?",
"create": "Create"
},
"start_empty": "Start with an empty automation"
},
"editor": {
"enable_disable": "Enable/Disable automation",
"introduction": "Use automations to bring your home alive.",
@ -1125,6 +1143,14 @@
"label": "Description",
"placeholder": "Optional description"
},
"blueprint": {
"header": "Blueprint",
"blueprint_to_use": "Blueprint to use",
"no_blueprints": "You don't have any blueprints",
"manage_blueprints": "Manage Blueprints",
"inputs": "Inputs",
"no_inputs": "This blueprint doesn't have any inputs."
},
"modes": {
"label": "Mode",
"description": "The mode controls what happens when the automation is triggered while the actions are still running from a previous trigger. Check the {documentation_link} for more info.",
@ -1414,6 +1440,31 @@
}
}
},
"blueprint": {
"caption": "Blueprints",
"description": "Manage blueprints",
"overview": {
"header": "Blueprint Editor",
"introduction": "The blueprint editor allows you to create and edit blueprints.",
"learn_more": "Learn more about blueprints",
"headers": {
"name": "Name"
},
"confirm_delete_header": "Delete this Blueprint?",
"confirm_delete_text": "Are you sure you want to delete this Blueprint"
},
"add": {
"header": "Add new blueprint",
"import_header": "Import {name} ({domain})",
"import_introduction": "You can import Blueprints of other users from Github and the community forums. Enter the url of the Blueprint below.",
"url": "Url of the blueprint",
"importing": "Importing blueprint...",
"import_btn": "Import blueprint",
"saving": "Saving blueprint...",
"save_btn": "Save blueprint",
"error_no_url": "Please enter the url of the blueprint."
}
},
"script": {
"caption": "Scripts",
"description": "Manage scripts",