Convert automation actions/scripts to Lit (#4324)

* Convert automation actions/scripts to Lit

* Update ha-automation-action-row.ts

* Comments
This commit is contained in:
Bram Kragten 2019-12-06 12:14:45 +01:00 committed by GitHub
parent 073428849e
commit 393adacc9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 892 additions and 990 deletions

View File

@ -5,6 +5,7 @@ import {
import { HomeAssistant } from "../types";
import { navigate } from "../common/navigate";
import { DeviceCondition, DeviceTrigger } from "./device_automation";
import { Action } from "./script";
export interface AutomationEntity extends HassEntityBase {
attributes: HassEntityAttributeBase & {
@ -18,7 +19,7 @@ export interface AutomationConfig {
description: string;
trigger: Trigger[];
condition?: Condition[];
action: any[];
action: Action[];
}
export interface ForDict {

View File

@ -1,5 +1,6 @@
import { HomeAssistant } from "../types";
import { computeObjectId } from "../common/entity/compute_object_id";
import { Condition } from "./automation";
export interface EventAction {
event: string;
@ -7,12 +8,40 @@ export interface EventAction {
event_data_template?: { [key: string]: any };
}
export interface ServiceAction {
service: string;
entity_id?: string;
data?: { [key: string]: any };
}
export interface DeviceAction {
device_id: string;
domain: string;
entity_id: string;
}
export interface DelayAction {
delay: number;
}
export interface SceneAction {
scene: string;
}
export interface WaitAction {
wait_template: string;
timeout?: number;
}
export type Action =
| EventAction
| DeviceAction
| ServiceAction
| Condition
| DelayAction
| SceneAction
| WaitAction;
export const triggerScript = (
hass: HomeAssistant,
entityId: string,

View File

@ -0,0 +1,272 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
// tslint:disable-next-line
import { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-menu-button/paper-menu-button";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
} from "lit-element";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-card";
import { HomeAssistant } from "../../../../types";
import { Action } from "../../../../data/script";
import "./types/ha-automation-action-service";
import "./types/ha-automation-action-device_id";
import "./types/ha-automation-action-delay";
import "./types/ha-automation-action-event";
import "./types/ha-automation-action-condition";
import "./types/ha-automation-action-scene";
import "./types/ha-automation-action-wait_template";
const OPTIONS = [
"condition",
"delay",
"device_id",
"event",
"scene",
"service",
"wait_template",
];
const getType = (action: Action) => {
return OPTIONS.find((option) => option in action);
};
declare global {
// for fire event
interface HASSDomEvents {
"move-action": { direction: "up" | "down" };
}
}
export interface ActionElement extends LitElement {
action: Action;
}
export const handleChangeEvent = (element: ActionElement, ev: CustomEvent) => {
ev.stopPropagation();
const name = (ev.target as any)?.name;
if (!name) {
return;
}
const newVal = ev.detail.value;
if ((element.action[name] || "") === newVal) {
return;
}
let newAction: Action;
if (!newVal) {
newAction = { ...element.action };
delete newAction[name];
} else {
newAction = { ...element.action, [name]: newVal };
}
fireEvent(element, "value-changed", { value: newAction });
};
@customElement("ha-automation-action-row")
export default class HaAutomationActionRow extends LitElement {
@property() public hass!: HomeAssistant;
@property() public action!: Action;
@property() public index!: number;
@property() public totalActions!: number;
@property() private _yamlMode = false;
protected render() {
const type = getType(this.action);
const selected = type ? OPTIONS.indexOf(type) : -1;
const yamlMode = this._yamlMode || selected === -1;
return html`
<ha-card>
<div class="card-content">
<div class="card-menu">
${this.index !== 0
? html`
<paper-icon-button
icon="hass:arrow-up"
@click=${this._moveUp}
></paper-icon-button>
`
: ""}
${this.index !== this.totalActions - 1
? html`
<paper-icon-button
icon="hass:arrow-down"
@click=${this._moveDown}
></paper-icon-button>
`
: ""}
<paper-menu-button
no-animations
horizontal-align="right"
horizontal-offset="-5"
vertical-offset="-5"
close-on-activate
>
<paper-icon-button
icon="hass:dots-vertical"
slot="dropdown-trigger"
></paper-icon-button>
<paper-listbox slot="dropdown-content">
<paper-item
@click=${this._switchYamlMode}
.disabled=${selected === -1}
>
${yamlMode
? this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
)
: this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
</paper-item>
<paper-item disabled>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
</paper-item>
<paper-item @click=${this._onDelete}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
</paper-item>
</paper-listbox>
</paper-menu-button>
</div>
${yamlMode
? html`
<div style="margin-right: 24px;">
${selected === -1
? html`
${this.hass.localize(
"ui.panel.config.automation.editor.actions.unsupported_action",
"action",
type
)}
`
: ""}
<ha-yaml-editor
.value=${this.action}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
</div>
`
: html`
<paper-dropdown-menu-light
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type_select"
)}
no-animations
>
<paper-listbox
slot="dropdown-content"
.selected=${selected}
@iron-select=${this._typeChanged}
>
${OPTIONS.map(
(opt) => html`
<paper-item .action=${opt}>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.type.${opt}.label`
)}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
<div>
${dynamicElement(`ha-automation-action-${type}`, {
hass: this.hass,
action: this.action,
})}
</div>
`}
</div>
</ha-card>
`;
}
private _moveUp() {
fireEvent(this, "move-action", { direction: "up" });
}
private _moveDown() {
fireEvent(this, "move-action", { direction: "down" });
}
private _onDelete() {
if (
confirm(
this.hass.localize(
"ui.panel.config.automation.editor.actions.delete_confirm"
)
)
) {
fireEvent(this, "value-changed", { value: null });
}
}
private _typeChanged(ev: CustomEvent) {
const type = ((ev.target as PaperListboxElement)?.selectedItem as any)
?.action;
if (!type) {
return;
}
if (type !== getType(this.action)) {
const elClass = customElements.get(`ha-automation-action-${type}`);
fireEvent(this, "value-changed", {
value: {
...elClass.defaultConfig,
},
});
}
}
private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", { value: ev.detail.value });
}
private _switchYamlMode() {
this._yamlMode = !this._yamlMode;
}
static get styles(): CSSResult {
return css`
.card-menu {
position: absolute;
top: 0;
right: 0;
z-index: 3;
color: var(--primary-text-color);
}
.rtl .card-menu {
right: auto;
left: 0;
}
.card-menu paper-item {
cursor: pointer;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-row": HaAutomationActionRow;
}
}

View File

@ -0,0 +1,98 @@
import "@material/mwc-button";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
} from "lit-element";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-card";
import { Action } from "../../../../data/script";
import { HomeAssistant } from "../../../../types";
import "./ha-automation-action-row";
@customElement("ha-automation-action")
export default class HaAutomationAction extends LitElement {
@property() public hass!: HomeAssistant;
@property() public actions!: Action[];
protected render() {
return html`
${this.actions.map(
(action, idx) => html`
<ha-automation-action-row
.index=${idx}
.totalActions=${this.actions.length}
.action=${action}
@move-action=${this._move}
@value-changed=${this._actionChanged}
.hass=${this.hass}
></ha-automation-action-row>
`
)}
<ha-card>
<div class="card-actions add-card">
<mwc-button @click=${this._addAction}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.add"
)}
</mwc-button>
</div>
</ha-card>
`;
}
private _addAction() {
const actions = this.actions.concat({
service: "",
});
fireEvent(this, "value-changed", { value: actions });
}
private _move(ev: CustomEvent) {
const index = (ev.target as any).index;
const newIndex = ev.detail.direction === "up" ? index - 1 : index + 1;
const actions = this.actions.concat();
const action = actions.splice(index, 1)[0];
actions.splice(newIndex, 0, action);
fireEvent(this, "value-changed", { value: actions });
}
private _actionChanged(ev: CustomEvent) {
ev.stopPropagation();
const actions = [...this.actions];
const newValue = ev.detail.value;
const index = (ev.target as any).index;
if (newValue === null) {
actions.splice(index, 1);
} else {
actions[index] = newValue;
}
fireEvent(this, "value-changed", { value: actions });
}
static get styles(): CSSResult {
return css`
ha-automation-action-row,
ha-card {
display: block;
margin-top: 16px;
}
.add-card mwc-button {
display: block;
text-align: center;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action": HaAutomationAction;
}
}

View File

@ -0,0 +1,41 @@
import "../../condition/ha-automation-condition-editor";
import { LitElement, property, customElement, html } from "lit-element";
import { ActionElement } from "../ha-automation-action-row";
import { HomeAssistant } from "../../../../../types";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { Condition } from "../../../../../data/automation";
@customElement("ha-automation-action-condition")
export class HaConditionAction extends LitElement implements ActionElement {
@property() public hass!: HomeAssistant;
@property() public action!: Condition;
public static get defaultConfig() {
return { condition: "state" };
}
public render() {
return html`
<ha-automation-condition-editor
.condition=${this.action}
.hass=${this.hass}
@value-changed=${this._conditionChanged}
></ha-automation-condition-editor>
`;
}
private _conditionChanged(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: ev.detail.value,
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-condition": HaConditionAction;
}
}

View File

@ -0,0 +1,44 @@
import "@polymer/paper-input/paper-input";
import "../../../../../components/ha-service-picker";
import "../../../../../components/entity/ha-entity-picker";
import "../../../../../components/ha-yaml-editor";
import { LitElement, property, customElement, html } from "lit-element";
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
import { HomeAssistant } from "../../../../../types";
import { DelayAction } from "../../../../../data/script";
@customElement("ha-automation-action-delay")
export class HaDelayAction extends LitElement implements ActionElement {
@property() public hass!: HomeAssistant;
@property() public action!: DelayAction;
public static get defaultConfig() {
return { delay: "" };
}
public render() {
const { delay } = this.action;
return html`
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.delay.delay"
)}
name="delay"
.value=${delay}
@value-changed=${this._valueChanged}
></paper-input>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-delay": HaDelayAction;
}
}

View File

@ -0,0 +1,129 @@
import "../../../../../components/device/ha-device-picker";
import "../../../../../components/device/ha-device-action-picker";
import "../../../../../components/ha-form/ha-form";
import {
fetchDeviceActionCapabilities,
deviceAutomationsEqual,
DeviceAction,
} from "../../../../../data/device_automation";
import { LitElement, customElement, property, html } from "lit-element";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { HomeAssistant } from "../../../../../types";
@customElement("ha-automation-action-device_id")
export class HaDeviceAction extends LitElement {
@property() public hass!: HomeAssistant;
@property() public action!: DeviceAction;
@property() private _deviceId?: string;
@property() private _capabilities?;
private _origAction?: DeviceAction;
public static get defaultConfig() {
return {
device_id: "",
domain: "",
entity_id: "",
};
}
protected render() {
const deviceId = this._deviceId || this.action.device_id;
const extraFieldsData =
this._capabilities && this._capabilities.extra_fields
? this._capabilities.extra_fields.map((item) => {
return { [item.name]: this.action[item.name] };
})
: undefined;
return html`
<ha-device-picker
.value=${deviceId}
@value-changed=${this._devicePicked}
.hass=${this.hass}
label="Device"
></ha-device-picker>
<ha-device-action-picker
.value=${this.action}
.deviceId=${deviceId}
@value-changed=${this._deviceActionPicked}
.hass=${this.hass}
label="Action"
></ha-device-action-picker>
${extraFieldsData
? html`
<ha-form
.data=${Object.assign({}, ...extraFieldsData)}
.schema=${this._capabilities.extra_fields}
.computeLabel=${this._extraFieldsComputeLabelCallback(
this.hass.localize
)}
@value-changed=${this._extraFieldsChanged}
></ha-form>
`
: ""}
`;
}
protected firstUpdated() {
if (!this._capabilities) {
this._getCapabilities();
}
if (this.action) {
this._origAction = this.action;
}
}
protected updated(changedPros) {
const prevAction = changedPros.get("action");
if (prevAction && !deviceAutomationsEqual(prevAction, this.action)) {
this._getCapabilities();
}
}
private async _getCapabilities() {
const action = this.action;
this._capabilities = action.domain
? await fetchDeviceActionCapabilities(this.hass, action)
: null;
}
private _devicePicked(ev) {
ev.stopPropagation();
this._deviceId = ev.target.value;
}
private _deviceActionPicked(ev) {
ev.stopPropagation();
let action = ev.detail.value;
if (this._origAction && deviceAutomationsEqual(this._origAction, action)) {
action = this._origAction;
}
fireEvent(this, "value-changed", { value: action });
}
private _extraFieldsChanged(ev) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
...this.action,
...ev.detail.value,
},
});
}
private _extraFieldsComputeLabelCallback(localize) {
// Returns a callback for ha-form to calculate labels per schema object
return (schema) =>
localize(
`ui.panel.config.automation.editor.actions.type.device.extra_fields.${schema.name}`
) || schema.name;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-device_id": HaDeviceAction;
}
}

View File

@ -0,0 +1,53 @@
import "@polymer/paper-input/paper-input";
import "../../../../../components/ha-service-picker";
import "../../../../../components/entity/ha-entity-picker";
import "../../../../../components/ha-yaml-editor";
import { LitElement, property, customElement } from "lit-element";
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
import { HomeAssistant } from "../../../../../types";
import { html } from "lit-html";
import { EventAction } from "../../../../../data/script";
@customElement("ha-automation-action-event")
export class HaEventAction extends LitElement implements ActionElement {
@property() public hass!: HomeAssistant;
@property() public action!: EventAction;
public static get defaultConfig(): EventAction {
return { event: "", event_data: {} };
}
public render() {
const { event, event_data } = this.action;
return html`
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.event.event"
)}
name="event"
.value=${event}
@value-changed=${this._valueChanged}
></paper-input>
<ha-yaml-editor
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.event.service_data"
)}
.name=${"event_data"}
.value=${event_data}
@value-changed=${this._valueChanged}
></ha-yaml-editor>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-event": HaEventAction;
}
}

View File

@ -0,0 +1,45 @@
import "../../../../../components/entity/ha-entity-picker";
import { LitElement, property, customElement, html } from "lit-element";
import { ActionElement } from "../ha-automation-action-row";
import { HomeAssistant } from "../../../../../types";
import { PolymerChangedEvent } from "../../../../../polymer-types";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { SceneAction } from "../../../../../data/script";
@customElement("ha-automation-action-scene")
export class HaSceneAction extends LitElement implements ActionElement {
@property() public hass!: HomeAssistant;
@property() public action!: SceneAction;
public static get defaultConfig(): SceneAction {
return { scene: "" };
}
protected render() {
const { scene } = this.action;
return html`
<ha-entity-picker
.hass=${this.hass}
.value=${scene}
@value-changed=${this._entityPicked}
.includeDomains=${["scene"]}
allow-custom-entity
></ha-entity-picker>
`;
}
private _entityPicked(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this.action, scene: ev.detail.value },
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-scene": HaSceneAction;
}
}

View File

@ -0,0 +1,107 @@
import "@polymer/paper-input/paper-input";
import "../../../../../components/ha-service-picker";
import "../../../../../components/entity/ha-entity-picker";
import "../../../../../components/ha-yaml-editor";
import { LitElement, property, customElement } from "lit-element";
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
import { HomeAssistant } from "../../../../../types";
import { html } from "lit-html";
import memoizeOne from "memoize-one";
import { computeDomain } from "../../../../../common/entity/compute_domain";
import { computeObjectId } from "../../../../../common/entity/compute_object_id";
import { PolymerChangedEvent } from "../../../../../polymer-types";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { ServiceAction } from "../../../../../data/script";
@customElement("ha-automation-action-service")
export class HaServiceAction extends LitElement implements ActionElement {
@property() public hass!: HomeAssistant;
@property() public action!: ServiceAction;
public static get defaultConfig() {
return { service: "", data: {} };
}
private _getServiceData = memoizeOne((service: string) => {
if (!service) {
return [];
}
const domain = computeDomain(service);
const serviceName = computeObjectId(service);
const serviceDomains = this.hass.services;
if (!(domain in serviceDomains)) {
return [];
}
if (!(serviceName in serviceDomains[domain])) {
return [];
}
const fields = serviceDomains[domain][serviceName].fields;
return Object.keys(fields).map((field) => {
return { key: field, ...fields[field] };
});
});
public render() {
const { service, data, entity_id } = this.action;
const serviceData = this._getServiceData(service);
const entity = serviceData.find((attr) => attr.key === "entity_id");
return html`
<ha-service-picker
.hass=${this.hass}
.value=${service}
@value-changed=${this._serviceChanged}
></ha-service-picker>
${entity
? html`
<ha-entity-picker
.hass=${this.hass}
.value=${entity_id}
.label=${entity.description}
@value-changed=${this._entityPicked}
.includeDomains=${[computeDomain(service)]}
allow-custom-entity
></ha-entity-picker>
`
: ""}
<ha-yaml-editor
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.service.service_data"
)}
.name=${"data"}
.value=${data}
@value-changed=${this._valueChanged}
></ha-yaml-editor>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
private _serviceChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
if (ev.detail.value === this.action.service) {
return;
}
fireEvent(this, "value-changed", {
value: { ...this.action, service: ev.detail.value },
});
}
private _entityPicked(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this.action, entity_id: ev.detail.value },
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-service": HaServiceAction;
}
}

View File

@ -0,0 +1,51 @@
import "@polymer/paper-input/paper-input";
import { LitElement, property, customElement } from "lit-element";
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
import { HomeAssistant } from "../../../../../types";
import { html } from "lit-html";
import { WaitAction } from "../../../../../data/script";
@customElement("ha-automation-action-wait_template")
export class HaWaitAction extends LitElement implements ActionElement {
@property() public hass!: HomeAssistant;
@property() public action!: WaitAction;
public static get defaultConfig() {
return { wait_template: "", timeout: "" };
}
protected render() {
const { wait_template, timeout } = this.action;
return html`
<ha-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.wait_template.wait_template"
)}
name="wait_template"
.value=${wait_template}
@value-changed=${this._valueChanged}
dir="ltr"
></ha-textarea>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.wait_template.timeout"
)}
.name=${"timeout"}
.value=${timeout}
@value-changed=${this._valueChanged}
></paper-input>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-wait_template": HaWaitAction;
}
}

View File

@ -38,9 +38,6 @@ export default class HaAutomationConditionEditor extends LitElement {
@property() public yamlMode = false;
protected render() {
if (!this.condition) {
return html``;
}
const selected = OPTIONS.indexOf(this.condition.condition);
const yamlMode = this.yamlMode || selected === -1;
return html`

View File

@ -28,9 +28,8 @@ export class HaDeviceCondition extends LitElement {
}
protected render() {
if (this._deviceId === undefined) {
this._deviceId = this.condition.device_id;
}
const deviceId = this._deviceId || this.condition.device_id;
const extraFieldsData =
this._capabilities && this._capabilities.extra_fields
? this._capabilities.extra_fields.map((item) => {
@ -40,14 +39,14 @@ export class HaDeviceCondition extends LitElement {
return html`
<ha-device-picker
.value=${this._deviceId}
.value=${deviceId}
@value-changed=${this._devicePicked}
.hass=${this.hass}
label="Device"
></ha-device-picker>
<ha-device-condition-picker
.value=${this.condition}
.deviceId=${this._deviceId}
.deviceId=${deviceId}
@value-changed=${this._deviceConditionPicked}
.hass=${this.hass}
label="Condition"

View File

@ -82,9 +82,6 @@ export default class HaAutomationTriggerRow extends LitElement {
@property() private _yamlMode = false;
protected render() {
if (!this.trigger) {
return html``;
}
const selected = OPTIONS.indexOf(this.trigger.platform);
const yamlMode = this._yamlMode || selected === -1;

View File

@ -28,9 +28,8 @@ export class HaDeviceTrigger extends LitElement {
}
protected render() {
if (this._deviceId === undefined) {
this._deviceId = this.trigger.device_id;
}
const deviceId = this._deviceId || this.trigger.device_id;
const extraFieldsData =
this._capabilities && this._capabilities.extra_fields
? this._capabilities.extra_fields.map((item) => {
@ -40,14 +39,14 @@ export class HaDeviceTrigger extends LitElement {
return html`
<ha-device-picker
.value=${this._deviceId}
.value=${deviceId}
@value-changed=${this._devicePicked}
.hass=${this.hass}
label="Device"
></ha-device-picker>
<ha-device-trigger-picker
.value=${this.trigger}
.deviceId=${this._deviceId}
.deviceId=${deviceId}
@value-changed=${this._deviceTriggerPicked}
.hass=${this.hass}
label="Trigger"

View File

@ -1,23 +0,0 @@
import { h, Component, ComponentChild } from "preact";
export class AutomationComponent<P = {}, S = {}> extends Component<P, S> {
// @ts-ignore
protected initialized: boolean;
constructor(props?, context?) {
super(props, context);
this.initialized = false;
}
public componentDidMount() {
this.initialized = true;
}
public componentWillUnmount() {
this.initialized = false;
}
public render(_props?, _state?, _context?: any): ComponentChild {
return <div />;
}
}

View File

@ -7,8 +7,7 @@ import "../../../components/ha-textarea";
import "../automation/trigger/ha-automation-trigger";
import "../automation/condition/ha-automation-condition";
import Script from "./script/index";
import "../automation/action/ha-automation-action";
export default class Automation extends Component<any> {
constructor() {
@ -38,8 +37,8 @@ export default class Automation extends Component<any> {
});
}
public actionChanged(action) {
this.props.onChange({ ...this.props.automation, action });
public actionChanged(ev: CustomEvent) {
this.props.onChange({ ...this.props.automation, action: ev.detail.value });
}
public render({ automation, isWide, hass, localize }) {
@ -144,11 +143,10 @@ export default class Automation extends Component<any> {
{localize("ui.panel.config.automation.editor.actions.learn_more")}
</a>
</span>
<Script
script={action}
onChange={this.actionChanged}
<ha-automation-action
actions={action}
onvalue-changed={this.actionChanged}
hass={hass}
localize={localize}
/>
</ha-config-section>
</div>

View File

@ -1,65 +0,0 @@
import { h, Component } from "preact";
import "../../../components/ha-textarea";
export default class JSONTextArea extends Component<any, any> {
constructor(props) {
super(props);
this.state = {
isvalid: true,
value: JSON.stringify(props.value || {}, null, 2),
};
this.onChange = this.onChange.bind(this);
}
public onChange(ev) {
const value = ev.target.value;
let parsed;
let isValid;
try {
parsed = JSON.parse(value);
isValid = true;
} catch (err) {
// Invalid JSON
isValid = false;
}
this.setState({
value,
isValid,
});
if (isValid) {
this.props.onChange(parsed);
}
}
public componentWillReceiveProps({ value }) {
if (value === this.props.value) {
return;
}
this.setState({
value: JSON.stringify(value, null, 2),
isValid: true,
});
}
public render({ label }, { value, isValid }) {
const style: any = {
minWidth: 300,
width: "100%",
};
if (!isValid) {
style.border = "1px solid red";
}
return (
<ha-textarea
label={label}
value={value}
style={style}
onvalue-changed={this.onChange}
dir="ltr"
/>
);
}
}

View File

@ -26,6 +26,7 @@ declare global {
"ha-automation-trigger": any;
"ha-automation-condition": any;
"ha-automation-condition-editor": any;
"ha-automation-action": any;
"ha-device-trigger-picker": any;
"ha-device-action-picker": any;
"ha-form": any;

View File

@ -4,7 +4,7 @@ import "@polymer/paper-input/paper-input";
import "../ha-config-section";
import "../../../components/ha-card";
import Script from "./script/index";
import "../automation/action/ha-automation-action";
export default class ScriptEditor extends Component<{
onChange: (...args: any[]) => any;
@ -27,8 +27,8 @@ export default class ScriptEditor extends Component<{
});
}
public sequenceChanged(sequence) {
this.props.onChange({ ...this.props.script, sequence });
public sequenceChanged(ev: CustomEvent) {
this.props.onChange({ ...this.props.script, sequence: ev.detail.value });
}
// @ts-ignore
@ -68,11 +68,10 @@ export default class ScriptEditor extends Component<{
</a>
</p>
</span>
<Script
script={sequence}
onChange={this.sequenceChanged}
<ha-automation-action
actions={sequence}
onvalue-changed={this.sequenceChanged}
hass={hass}
localize={localize}
/>
</ha-config-section>
</div>

View File

@ -1,116 +0,0 @@
import { h, Component } from "preact";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-item/paper-item";
import YAMLTextArea from "../yaml_textarea";
import CallServiceAction from "./call_service";
import ConditionAction from "./condition";
import DelayAction from "./delay";
import DeviceAction from "./device";
import EventAction from "./event";
import SceneAction from "./scene";
import WaitAction from "./wait";
const TYPES = {
service: CallServiceAction,
delay: DelayAction,
wait_template: WaitAction,
condition: ConditionAction,
event: EventAction,
device_id: DeviceAction,
scene: SceneAction,
};
const OPTIONS = Object.keys(TYPES).sort();
function getType(action) {
const keys = Object.keys(TYPES);
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < keys.length; i++) {
if (keys[i] in action) {
return keys[i];
}
}
return null;
}
export default class Action extends Component<any> {
constructor() {
super();
this.typeChanged = this.typeChanged.bind(this);
this.onYamlChange = this.onYamlChange.bind(this);
}
public typeChanged(ev) {
const newType = ev.target.selectedItem.attributes.action.value;
const oldType = getType(this.props.action);
if (oldType !== newType) {
this.props.onChange(this.props.index, TYPES[newType].defaultConfig);
}
}
public render({ index, action, onChange, hass, localize, yamlMode }) {
const type = getType(action);
// tslint:disable-next-line: variable-name
const Comp = type && TYPES[type];
// @ts-ignore
const selected = OPTIONS.indexOf(type);
if (yamlMode || !Comp) {
return (
<div style="margin-right: 24px;">
{!Comp && (
<div>
{localize(
"ui.panel.config.automation.editor.actions.unsupported_action",
"action",
type
)}
</div>
)}
<YAMLTextArea value={action} onChange={this.onYamlChange} />
</div>
);
}
return (
<div>
<paper-dropdown-menu-light
label={localize(
"ui.panel.config.automation.editor.actions.type_select"
)}
no-animations
>
<paper-listbox
slot="dropdown-content"
selected={selected}
oniron-select={this.typeChanged}
>
{OPTIONS.map((opt) => (
<paper-item action={opt}>
{localize(
`ui.panel.config.automation.editor.actions.type.${opt}.label`
)}
</paper-item>
))}
</paper-listbox>
</paper-dropdown-menu-light>
<Comp
index={index}
action={action}
onChange={onChange}
hass={hass}
localize={localize}
/>
</div>
);
}
private onYamlChange(condition) {
this.props.onChange(this.props.index, condition);
}
}

View File

@ -1,96 +0,0 @@
import { h, Component } from "preact";
import "@polymer/paper-menu-button/paper-menu-button";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "../../../../components/ha-card";
import ActionEdit from "./action_edit";
export default class Action extends Component<any> {
public state: { yamlMode: boolean };
private moveUp: (event: Event) => void;
private moveDown: (event: Event) => void;
constructor(props) {
super(props);
this.state = {
yamlMode: false,
};
this.onDelete = this.onDelete.bind(this);
this.switchYamlMode = this.switchYamlMode.bind(this);
this.moveUp = props.moveUp.bind(this, props.index);
this.moveDown = props.moveDown.bind(this, props.index);
}
public onDelete() {
// eslint-disable-next-line
if (
confirm(
this.props.localize(
"ui.panel.config.automation.editor.actions.delete_confirm"
)
)
) {
this.props.onChange(this.props.index, null);
}
}
public render(props, { yamlMode }) {
return (
<ha-card>
<div class="card-content">
<div class="card-menu" style="z-index: 3">
{props.index !== 0 && (
<paper-icon-button icon="hass:arrow-up" onTap={this.moveUp} />
)}
{props.index !== props.length - 1 && (
<paper-icon-button icon="hass:arrow-down" onTap={this.moveDown} />
)}
<paper-menu-button
no-animations
horizontal-align="right"
horizontal-offset="-5"
vertical-offset="-5"
close-on-activate
>
<paper-icon-button
icon="hass:dots-vertical"
slot="dropdown-trigger"
/>
<paper-listbox slot="dropdown-content">
<paper-item onTap={this.switchYamlMode}>
{yamlMode
? props.localize(
"ui.panel.config.automation.editor.edit_ui"
)
: props.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
</paper-item>
<paper-item disabled>
{props.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
</paper-item>
<paper-item onTap={this.onDelete}>
{props.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
</paper-item>
</paper-listbox>
</paper-menu-button>
</div>
<ActionEdit {...props} yamlMode={yamlMode} />
</div>
</ha-card>
);
}
private switchYamlMode() {
this.setState({
yamlMode: !this.state.yamlMode,
});
}
}

View File

@ -1,112 +0,0 @@
import { h } from "preact";
import "../../../../components/ha-service-picker";
import "../../../../components/entity/ha-entity-picker";
import YAMLTextArea from "../yaml_textarea";
import { AutomationComponent } from "../automation-component";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { computeObjectId } from "../../../../common/entity/compute_object_id";
import memoizeOne from "memoize-one";
export default class CallServiceAction extends AutomationComponent<any> {
private _getServiceData = memoizeOne((service: string) => {
if (!service) {
return [];
}
const domain = computeDomain(service);
const serviceName = computeObjectId(service);
const serviceDomains = this.props.hass.services;
if (!(domain in serviceDomains)) {
return [];
}
if (!(serviceName in serviceDomains[domain])) {
return [];
}
const fields = serviceDomains[domain][serviceName].fields;
return Object.keys(fields).map((field) => {
return { key: field, ...fields[field] };
});
});
constructor() {
super();
this.serviceChanged = this.serviceChanged.bind(this);
this.entityChanged = this.entityChanged.bind(this);
this.serviceDataChanged = this.serviceDataChanged.bind(this);
}
public serviceChanged(ev) {
if (!this.initialized) {
return;
}
const newAction = {
...this.props.action,
service: ev.target.value,
};
if (
computeDomain(this.props.action.service) !==
computeDomain(ev.target.value)
) {
delete newAction.entity_id;
}
this.props.onChange(this.props.index, newAction);
}
public entityChanged(ev) {
if (!this.initialized) {
return;
}
this.props.onChange(this.props.index, {
...this.props.action,
entity_id: ev.target.value,
});
}
public serviceDataChanged(data) {
if (!this.initialized) {
return;
}
this.props.onChange(this.props.index, { ...this.props.action, data });
}
public render({ action, hass, localize }) {
const { service, data, entity_id } = action;
const serviceData = this._getServiceData(service);
const entity = serviceData.find((attr) => attr.key === "entity_id");
return (
<div>
<ha-service-picker
hass={hass}
value={service}
onChange={this.serviceChanged}
/>
{entity && (
<ha-entity-picker
hass={hass}
value={entity_id}
label={entity.description}
onChange={this.entityChanged}
includeDomains={[computeDomain(service)]}
allow-custom-entity
/>
)}
<YAMLTextArea
label={localize(
"ui.panel.config.automation.editor.actions.type.service.service_data"
)}
value={data}
onChange={this.serviceDataChanged}
/>
</div>
);
}
}
(CallServiceAction as any).defaultConfig = {
alias: "",
service: "",
data: {},
};

View File

@ -1,32 +0,0 @@
import { h, Component } from "preact";
import "../../automation/condition/ha-automation-condition-editor";
export default class ConditionAction extends Component<any> {
constructor() {
super();
this.conditionChanged = this.conditionChanged.bind(this);
}
public conditionChanged(ev) {
this.props.onChange(this.props.index, ev.detail.value);
}
// eslint-disable-next-line
public render({ action, hass }) {
return (
<div>
<ha-automation-condition-editor
condition={action}
onvalue-changed={this.conditionChanged}
hass={hass}
/>
</div>
);
}
}
(ConditionAction as any).defaultConfig = {
condition: "state",
};

View File

@ -1,45 +0,0 @@
import { h } from "preact";
import "@polymer/paper-input/paper-input";
import { AutomationComponent } from "../automation-component";
export default class DelayAction extends AutomationComponent<any> {
constructor() {
super();
this.onChange = this.onChange.bind(this);
}
public render({ action, localize }) {
const { delay } = action;
return (
<div>
<paper-input
label={localize(
"ui.panel.config.automation.editor.actions.type.delay.delay"
)}
name="delay"
value={delay}
onvalue-changed={this.onChange}
/>
</div>
);
}
private onChange(ev) {
if (
!this.initialized ||
ev.target.value === this.props.action[ev.target.name]
) {
return;
}
this.props.onChange(this.props.index, {
...this.props.action,
[ev.target.name]: ev.target.value,
});
}
}
(DelayAction as any).defaultConfig = {
delay: "",
};

View File

@ -1,144 +0,0 @@
import { h } from "preact";
import "../../../../components/device/ha-device-picker";
import "../../../../components/device/ha-device-action-picker";
import "../../../../components/ha-form/ha-form";
import { AutomationComponent } from "../automation-component";
import {
fetchDeviceActionCapabilities,
deviceAutomationsEqual,
} from "../../../../data/device_automation";
import { DeviceAction } from "../../../../data/script";
import { HomeAssistant } from "../../../../types";
export default class DeviceActionEditor extends AutomationComponent<
{
index: number;
action: DeviceAction;
hass: HomeAssistant;
onChange(index: number, action: DeviceAction);
},
{
device_id: string | undefined;
capabilities: any | undefined;
}
> {
public static defaultConfig: DeviceAction = {
device_id: "",
domain: "",
entity_id: "",
};
private _origAction;
constructor() {
super();
this.devicePicked = this.devicePicked.bind(this);
this.deviceActionPicked = this.deviceActionPicked.bind(this);
this._extraFieldsChanged = this._extraFieldsChanged.bind(this);
this.state = { device_id: undefined, capabilities: undefined };
}
public render() {
const { action, hass } = this.props;
const deviceId = this.state.device_id || action.device_id;
const capabilities = this.state.capabilities;
const extraFieldsData =
capabilities && capabilities.extra_fields
? capabilities.extra_fields.map((item) => {
return { [item.name]: this.props.action[item.name] };
})
: undefined;
return (
<div>
<ha-device-picker
value={deviceId}
onChange={this.devicePicked}
hass={hass}
label="Device"
/>
<ha-device-action-picker
value={action}
deviceId={deviceId}
onChange={this.deviceActionPicked}
hass={hass}
label="Action"
/>
{extraFieldsData && (
<ha-form
data={Object.assign({}, ...extraFieldsData)}
onvalue-changed={this._extraFieldsChanged}
schema={this.state.capabilities.extra_fields}
computeLabel={this._extraFieldsComputeLabelCallback(hass.localize)}
/>
)}
</div>
);
}
public componentDidMount() {
this.initialized = true;
if (!this.state.capabilities) {
this._getCapabilities();
}
if (this.props.action) {
this._origAction = this.props.action;
}
}
public componentDidUpdate(prevProps) {
if (!deviceAutomationsEqual(prevProps.action, this.props.action)) {
this._getCapabilities();
}
}
private devicePicked(ev) {
if (!this.initialized) {
return;
}
this.setState({ ...this.state, device_id: ev.target.value });
}
private deviceActionPicked(ev) {
if (!this.initialized) {
return;
}
let deviceAction = ev.target.value;
if (
this._origAction &&
deviceAutomationsEqual(this._origAction, deviceAction)
) {
deviceAction = this._origAction;
}
this.props.onChange(this.props.index, deviceAction);
}
private async _getCapabilities() {
const action = this.props.action;
const capabilities = action.domain
? await fetchDeviceActionCapabilities(this.props.hass, action)
: null;
this.setState({ ...this.state, capabilities });
}
private _extraFieldsChanged(ev) {
if (!this.initialized) {
return;
}
this.props.onChange(this.props.index, {
...this.props.action,
...ev.detail.value,
});
}
private _extraFieldsComputeLabelCallback(localize) {
// Returns a callback for ha-form to calculate labels per schema object
return (schema) =>
localize(
`ui.panel.config.automation.editor.actions.type.device_id.extra_fields.${schema.name}`
) || schema.name;
}
}

View File

@ -1,70 +0,0 @@
import { h } from "preact";
import "@polymer/paper-input/paper-input";
import YAMLTextArea from "../yaml_textarea";
import { onChangeEvent } from "../../../../common/preact/event";
import { LocalizeFunc } from "../../../../common/translations/localize";
import { EventAction } from "../../../../data/script";
import { AutomationComponent } from "../automation-component";
interface Props {
index: number;
action: EventAction;
localize: LocalizeFunc;
onChange: (index: number, action: EventAction) => void;
}
export default class EventActionForm extends AutomationComponent<Props> {
private onChange: (event: Event) => void;
static get defaultConfig(): EventAction {
return {
event: "",
event_data: {},
};
}
constructor() {
super();
this.onChange = onChangeEvent.bind(this, "action");
this.serviceDataChanged = this.serviceDataChanged.bind(this);
}
public render() {
const {
action: { event, event_data },
localize,
} = this.props;
return (
<div>
<paper-input
label={localize(
"ui.panel.config.automation.editor.actions.type.event.event"
)}
name="event"
value={event}
onvalue-changed={this.onChange}
/>
<YAMLTextArea
label={localize(
"ui.panel.config.automation.editor.actions.type.event.service_data"
)}
value={event_data}
onChange={this.serviceDataChanged}
/>
</div>
);
}
private serviceDataChanged(eventData) {
if (!this.initialized) {
return;
}
this.props.onChange(this.props.index, {
...this.props.action,
event_data: eventData,
});
}
}

View File

@ -1,77 +0,0 @@
import { h, Component } from "preact";
import "@material/mwc-button";
import "../../../../components/ha-card";
import ActionRow from "./action_row";
export default class Script extends Component<any> {
constructor() {
super();
this.addAction = this.addAction.bind(this);
this.actionChanged = this.actionChanged.bind(this);
this.moveUp = this.moveUp.bind(this);
this.moveDown = this.moveDown.bind(this);
}
public addAction() {
const script = this.props.script.concat({
service: "",
});
this.props.onChange(script);
}
public actionChanged(index, newValue) {
const script = this.props.script.concat();
if (newValue === null) {
script.splice(index, 1);
} else {
script[index] = newValue;
}
this.props.onChange(script);
}
public moveUp(index: number) {
this.move(index, index - 1);
}
public moveDown(index: number) {
this.move(index, index + 1);
}
public render({ script, hass, localize }) {
return (
<div class="script">
{script.map((act, idx) => (
<ActionRow
index={idx}
length={script.length}
action={act}
onChange={this.actionChanged}
moveUp={this.moveUp}
moveDown={this.moveDown}
hass={hass}
localize={localize}
/>
))}
<ha-card>
<div class="card-actions add-card">
<mwc-button onTap={this.addAction}>
{localize("ui.panel.config.automation.editor.actions.add")}
</mwc-button>
</div>
</ha-card>
</div>
);
}
private move(index: number, newIndex: number) {
const script = this.props.script.concat();
const action = script.splice(index, 1)[0];
script.splice(newIndex, 0, action);
this.props.onChange(script);
}
}

View File

@ -1,38 +0,0 @@
import { h } from "preact";
import "../../../../components/entity/ha-entity-picker";
import { AutomationComponent } from "../automation-component";
export default class SceneAction extends AutomationComponent<any> {
constructor() {
super();
this.sceneChanged = this.sceneChanged.bind(this);
}
public sceneChanged(ev: any) {
this.props.onChange(this.props.index, {
...this.props.action,
scene: ev.target.value,
});
}
public render({ action, hass }) {
const { scene } = action;
return (
<div>
<ha-entity-picker
value={scene}
onChange={this.sceneChanged}
hass={hass}
includeDomains={["scene"]}
allowCustomEntity
/>
</div>
);
}
}
(SceneAction as any).defaultConfig = {
scene: "",
};

View File

@ -1,60 +0,0 @@
import { h } from "preact";
import "@polymer/paper-input/paper-input";
import "../../../../components/ha-textarea";
import { onChangeEvent } from "../../../../common/preact/event";
import { AutomationComponent } from "../automation-component";
export default class WaitAction extends AutomationComponent<any> {
private onChange: (obj: any) => void;
constructor() {
super();
this.onChange = onChangeEvent.bind(this, "action");
this.onTemplateChange = this.onTemplateChange.bind(this);
}
// Gets fired on mount. If empty, onChangeEvent removes attribute.
// Without the attribute this action is no longer matched to this component.
public onTemplateChange(ev) {
if (!this.initialized) {
return;
}
this.props.onChange(this.props.index, {
...this.props.action,
[ev.target.getAttribute("name")]: ev.target.value,
});
}
public render({ action, localize }) {
/* eslint-disable camelcase */
const { wait_template, timeout } = action;
return (
<div>
<ha-textarea
label={localize(
"ui.panel.config.automation.editor.actions.type.wait_template.wait_template"
)}
name="wait_template"
value={wait_template}
onvalue-changed={this.onTemplateChange}
dir="ltr"
/>
<paper-input
label={localize(
"ui.panel.config.automation.editor.actions.type.wait_template.timeout"
)}
name="timeout"
value={timeout}
onvalue-changed={this.onChange}
/>
</div>
);
}
}
(WaitAction as any).defaultConfig = {
wait_template: "",
timeout: "",
};

View File

@ -1,80 +0,0 @@
import { h, Component } from "preact";
import { safeDump, safeLoad } from "js-yaml";
import "../../../components/ha-code-editor";
const isEmpty = (obj: object) => {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
return false;
}
}
return true;
};
export default class YAMLTextArea extends Component<any, any> {
constructor(props) {
super(props);
let value: string | undefined;
try {
value =
props.value && !isEmpty(props.value)
? safeDump(props.value)
: undefined;
} catch (err) {
alert(`There was an error converting to YAML: ${err}`);
}
this.state = {
isvalid: true,
value,
};
this.onChange = this.onChange.bind(this);
}
public onChange(ev) {
const value = ev.detail.value;
let parsed;
let isValid = true;
if (value) {
try {
parsed = safeLoad(value);
isValid = true;
} catch (err) {
// Invalid YAML
isValid = false;
}
} else {
parsed = {};
}
this.setState({
value,
isValid,
});
if (isValid) {
this.props.onChange(parsed);
}
}
public render({ label }, { value, isValid }) {
const style: any = {
minWidth: 300,
width: "100%",
};
return (
<div>
{label && <p>{label}</p>}
<ha-code-editor
mode="yaml"
style={style}
value={value}
error={isValid === false}
onvalue-changed={this.onChange}
/>
</div>
);
}
}