From 011219b7274a0afbda4635217442193e250ddeef Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 6 Sep 2019 00:43:14 +0200 Subject: [PATCH] Add device conditions to automation editor. (#3595) * Add device conditions to automation editor. * Fix inheritance shizzle * Make device automation lists simple lists, not dicts * Really make device automation lists simple lists * Add few types * Fix types --- .../device/ha-device-automation-picker.ts | 195 ++++++++++++++++++ .../device/ha-device-condition-picker.ts | 35 ++++ .../device/ha-device-trigger-picker.ts | 166 ++------------- src/data/device_automation.ts | 59 ++++-- .../config/js/condition/condition_edit.js | 2 + src/panels/config/js/condition/device.js | 57 +++++ src/panels/config/js/trigger/device.js | 1 + src/translations/en.json | 3 + 8 files changed, 350 insertions(+), 168 deletions(-) create mode 100644 src/components/device/ha-device-automation-picker.ts create mode 100644 src/components/device/ha-device-condition-picker.ts create mode 100644 src/panels/config/js/condition/device.js diff --git a/src/components/device/ha-device-automation-picker.ts b/src/components/device/ha-device-automation-picker.ts new file mode 100644 index 0000000000..cd79540dd8 --- /dev/null +++ b/src/components/device/ha-device-automation-picker.ts @@ -0,0 +1,195 @@ +import "@polymer/paper-input/paper-input"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-item/paper-item-body"; +import "@polymer/paper-listbox/paper-listbox"; +import { + LitElement, + TemplateResult, + html, + css, + CSSResult, + property, +} from "lit-element"; +import { HomeAssistant } from "../../types"; +import { fireEvent } from "../../common/dom/fire_event"; +import { + DeviceAutomation, + deviceAutomationsEqual, +} from "../../data/device_automation"; +import "../../components/ha-paper-dropdown-menu"; + +const NO_AUTOMATION_KEY = "NO_AUTOMATION"; +const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION"; + +export abstract class HaDeviceAutomationPicker< + T extends DeviceAutomation +> extends LitElement { + public hass!: HomeAssistant; + @property() public label?: string; + @property() public deviceId?: string; + @property() public value?: T; + protected NO_AUTOMATION_TEXT = "No automations"; + protected UNKNOWN_AUTOMATION_TEXT = "Unknown automation"; + @property() private _automations: T[] = []; + + // Trigger an empty render so we start with a clean DOM. + // paper-listbox does not like changing things around. + @property() private _renderEmpty = false; + + private _localizeDeviceAutomation: ( + hass: HomeAssistant, + automation: T + ) => string; + private _fetchDeviceAutomations: ( + hass: HomeAssistant, + deviceId: string + ) => Promise; + private _createNoAutomation: (deviceId?: string) => T; + + constructor( + localizeDeviceAutomation: HaDeviceAutomationPicker< + T + >["_localizeDeviceAutomation"], + fetchDeviceAutomations: HaDeviceAutomationPicker< + T + >["_fetchDeviceAutomations"], + createNoAutomation: HaDeviceAutomationPicker["_createNoAutomation"] + ) { + super(); + this._localizeDeviceAutomation = localizeDeviceAutomation; + this._fetchDeviceAutomations = fetchDeviceAutomations; + this._createNoAutomation = createNoAutomation; + } + + private get _key() { + if ( + !this.value || + deviceAutomationsEqual( + this._createNoAutomation(this.deviceId), + this.value + ) + ) { + return NO_AUTOMATION_KEY; + } + + const idx = this._automations.findIndex((automation) => + deviceAutomationsEqual(automation, this.value!) + ); + + if (idx === -1) { + return UNKNOWN_AUTOMATION_KEY; + } + + return `${this._automations[idx].device_id}_${idx}`; + } + + protected render(): TemplateResult | void { + if (this._renderEmpty) { + return html``; + } + return html` + + + + + ${this._automations.map( + (automation, idx) => html` + + ${this._localizeDeviceAutomation(this.hass, automation)} + + ` + )} + + + `; + } + + protected updated(changedProps) { + super.updated(changedProps); + + if (changedProps.has("deviceId")) { + this._updateDeviceInfo(); + } + + // The value has changed, force the listbox to update + if (changedProps.has("value") || changedProps.has("_renderEmpty")) { + const listbox = this.shadowRoot!.querySelector("paper-listbox")!; + if (listbox) { + listbox._selectSelected(this._key); + } + } + } + + private async _updateDeviceInfo() { + this._automations = this.deviceId + ? await this._fetchDeviceAutomations(this.hass, this.deviceId) + : // No device, clear the list of automations + []; + + // If there is no value, or if we have changed the device ID, reset the value. + if (!this.value || this.value.device_id !== this.deviceId) { + this._setValue( + this._automations.length + ? this._automations[0] + : this._createNoAutomation(this.deviceId) + ); + } + this._renderEmpty = true; + await this.updateComplete; + this._renderEmpty = false; + } + + private _automationChanged(ev) { + this._setValue(ev.detail.item.automation); + } + + private _setValue(automation: T) { + if (this.value && deviceAutomationsEqual(automation, this.value)) { + return; + } + this.value = automation; + setTimeout(() => { + fireEvent(this, "change"); + }, 0); + } + + static get styles(): CSSResult { + return css` + ha-paper-dropdown-menu { + width: 100%; + } + paper-listbox { + min-width: 200px; + } + paper-item { + cursor: pointer; + } + `; + } +} diff --git a/src/components/device/ha-device-condition-picker.ts b/src/components/device/ha-device-condition-picker.ts new file mode 100644 index 0000000000..d0a45fe8e8 --- /dev/null +++ b/src/components/device/ha-device-condition-picker.ts @@ -0,0 +1,35 @@ +import { customElement } from "lit-element"; +import { + DeviceCondition, + fetchDeviceConditions, + localizeDeviceAutomationCondition, +} from "../../data/device_automation"; +import "../../components/ha-paper-dropdown-menu"; +import { HaDeviceAutomationPicker } from "./ha-device-automation-picker"; + +@customElement("ha-device-condition-picker") +class HaDeviceConditionPicker extends HaDeviceAutomationPicker< + DeviceCondition +> { + protected NO_AUTOMATION_TEXT = "No conditions"; + protected UNKNOWN_AUTOMATION_TEXT = "Unknown condition"; + + constructor() { + super( + localizeDeviceAutomationCondition, + fetchDeviceConditions, + (deviceId?: string) => ({ + device_id: deviceId || "", + condition: "device", + domain: "", + entity_id: "", + }) + ); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-device-condition-picker": HaDeviceConditionPicker; + } +} diff --git a/src/components/device/ha-device-trigger-picker.ts b/src/components/device/ha-device-trigger-picker.ts index 6330ae76d5..a5913220ac 100644 --- a/src/components/device/ha-device-trigger-picker.ts +++ b/src/components/device/ha-device-trigger-picker.ts @@ -1,164 +1,28 @@ -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; -import "@polymer/paper-listbox/paper-listbox"; -import { - LitElement, - TemplateResult, - html, - css, - CSSResult, - customElement, - property, -} from "lit-element"; -import { HomeAssistant } from "../../types"; -import { fireEvent } from "../../common/dom/fire_event"; +import { customElement } from "lit-element"; import { DeviceTrigger, fetchDeviceTriggers, - deviceAutomationTriggersEqual, localizeDeviceAutomationTrigger, } from "../../data/device_automation"; import "../../components/ha-paper-dropdown-menu"; - -const NO_TRIGGER_KEY = "NO_TRIGGER"; -const UNKNOWN_TRIGGER_KEY = "UNKNOWN_TRIGGER"; +import { HaDeviceAutomationPicker } from "./ha-device-automation-picker"; @customElement("ha-device-trigger-picker") -class HaDeviceTriggerPicker extends LitElement { - public hass!: HomeAssistant; - @property() public label?: string; - @property() public deviceId?: string; - @property() public value?: DeviceTrigger; - @property() private _triggers: DeviceTrigger[] = []; +class HaDeviceTriggerPicker extends HaDeviceAutomationPicker { + protected NO_AUTOMATION_TEXT = "No triggers"; + protected UNKNOWN_AUTOMATION_TEXT = "Unknown trigger"; - // Trigger an empty render so we start with a clean DOM. - // paper-listbox does not like changing things around. - @property() private _renderEmpty = false; - - private get _key() { - if ( - !this.value || - deviceAutomationTriggersEqual(this._noTrigger, this.value) - ) { - return NO_TRIGGER_KEY; - } - - const idx = this._triggers.findIndex((trigger) => - deviceAutomationTriggersEqual(trigger, this.value!) + constructor() { + super( + localizeDeviceAutomationTrigger, + fetchDeviceTriggers, + (deviceId?: string) => ({ + device_id: deviceId || "", + platform: "device", + domain: "", + entity_id: "", + }) ); - - if (idx === -1) { - return UNKNOWN_TRIGGER_KEY; - } - - return `${this._triggers[idx].device_id}_${idx}`; - } - - protected render(): TemplateResult | void { - if (this._renderEmpty) { - return html``; - } - return html` - - - - - ${this._triggers.map( - (trigger, idx) => html` - - ${localizeDeviceAutomationTrigger(this.hass, trigger)} - - ` - )} - - - `; - } - - protected updated(changedProps) { - super.updated(changedProps); - - if (changedProps.has("deviceId")) { - this._updateDeviceInfo(); - } - - // The value has changed, force the listbox to update - if (changedProps.has("value") || changedProps.has("_renderEmpty")) { - const listbox = this.shadowRoot!.querySelector("paper-listbox")!; - if (listbox) { - listbox._selectSelected(this._key); - } - } - } - - private async _updateDeviceInfo() { - this._triggers = this.deviceId - ? await fetchDeviceTriggers(this.hass!, this.deviceId!) - : // No device, clear the list of triggers - []; - - // If there is no value, or if we have changed the device ID, reset the value. - if (!this.value || this.value.device_id !== this.deviceId) { - this._setValue( - this._triggers.length ? this._triggers[0] : this._noTrigger - ); - } - this._renderEmpty = true; - await this.updateComplete; - this._renderEmpty = false; - } - - private get _noTrigger() { - return { - device_id: this.deviceId || "", - platform: "device", - domain: "", - entity_id: "", - }; - } - - private _triggerChanged(ev) { - this._setValue(ev.detail.item.trigger); - } - - private _setValue(trigger: DeviceTrigger) { - if (this.value && deviceAutomationTriggersEqual(trigger, this.value)) { - return; - } - this.value = trigger; - setTimeout(() => { - fireEvent(this, "change"); - }, 0); - } - - static get styles(): CSSResult { - return css` - ha-paper-dropdown-menu { - width: 100%; - } - paper-listbox { - min-width: 200px; - } - paper-item { - cursor: pointer; - } - `; } } diff --git a/src/data/device_automation.ts b/src/data/device_automation.ts index d3c7370527..a4330c7390 100644 --- a/src/data/device_automation.ts +++ b/src/data/device_automation.ts @@ -1,8 +1,7 @@ import { HomeAssistant } from "../types"; import compute_state_name from "../common/entity/compute_state_name"; -export interface DeviceTrigger { - platform: string; +export interface DeviceAutomation { device_id: string; domain: string; entity_id: string; @@ -10,21 +9,29 @@ export interface DeviceTrigger { event?: string; } -export interface DeviceTriggerList { - triggers: DeviceTrigger[]; +export interface DeviceCondition extends DeviceAutomation { + condition: string; } -export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) => - hass - .callWS({ - type: "device_automation/trigger/list", - device_id: deviceId, - }) - .then((response) => response.triggers); +export interface DeviceTrigger extends DeviceAutomation { + platform: string; +} -export const deviceAutomationTriggersEqual = ( - a: DeviceTrigger, - b: DeviceTrigger +export const fetchDeviceConditions = (hass: HomeAssistant, deviceId: string) => + hass.callWS({ + type: "device_automation/condition/list", + device_id: deviceId, + }); + +export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) => + hass.callWS({ + type: "device_automation/trigger/list", + device_id: deviceId, + }); + +export const deviceAutomationsEqual = ( + a: DeviceAutomation, + b: DeviceAutomation ) => { if (typeof a !== typeof b) { return false; @@ -44,14 +51,32 @@ export const deviceAutomationTriggersEqual = ( return true; }; +export const localizeDeviceAutomationCondition = ( + hass: HomeAssistant, + condition: DeviceCondition +) => { + const state = condition.entity_id + ? hass.states[condition.entity_id] + : undefined; + return hass.localize( + `component.${condition.domain}.device_automation.condition_type.${ + condition.type + }`, + "name", + state ? compute_state_name(state) : "" + ); +}; + export const localizeDeviceAutomationTrigger = ( hass: HomeAssistant, trigger: DeviceTrigger -) => - hass.localize( +) => { + const state = trigger.entity_id ? hass.states[trigger.entity_id] : undefined; + return hass.localize( `component.${trigger.domain}.device_automation.trigger_type.${ trigger.type }`, "name", - trigger.entity_id ? compute_state_name(hass!.states[trigger.entity_id]) : "" + state ? compute_state_name(state) : "" ); +}; diff --git a/src/panels/config/js/condition/condition_edit.js b/src/panels/config/js/condition/condition_edit.js index 3012aca885..5609ef8a96 100644 --- a/src/panels/config/js/condition/condition_edit.js +++ b/src/panels/config/js/condition/condition_edit.js @@ -3,6 +3,7 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-item/paper-item"; +import DeviceCondition from "./device"; import NumericStateCondition from "./numeric_state"; import StateCondition from "./state"; import SunCondition from "./sun"; @@ -11,6 +12,7 @@ import TimeCondition from "./time"; import ZoneCondition from "./zone"; const TYPES = { + device: DeviceCondition, state: StateCondition, numeric_state: NumericStateCondition, sun: SunCondition, diff --git a/src/panels/config/js/condition/device.js b/src/panels/config/js/condition/device.js new file mode 100644 index 0000000000..fdcb6a02e8 --- /dev/null +++ b/src/panels/config/js/condition/device.js @@ -0,0 +1,57 @@ +import { h, Component } from "preact"; + +import "../../../../components/device/ha-device-picker"; +import "../../../../components/device/ha-device-condition-picker"; + +import { onChangeEvent } from "../../../../common/preact/event"; + +export default class DeviceCondition extends Component { + constructor() { + super(); + this.onChange = onChangeEvent.bind(this, "condition"); + this.devicePicked = this.devicePicked.bind(this); + this.deviceConditionPicked = this.deviceConditionPicked.bind(this); + this.state.device_id = undefined; + } + + devicePicked(ev) { + this.setState({ device_id: ev.target.value }); + } + + deviceConditionPicked(ev) { + const deviceCondition = ev.target.value; + this.props.onChange( + this.props.index, + (this.props.condition = deviceCondition) + ); + } + + /* eslint-disable camelcase */ + render({ condition, hass }, { device_id }) { + if (device_id === undefined) device_id = condition.device_id; + + return ( +
+ + +
+ ); + } +} + +DeviceCondition.defaultConfig = { + device_id: "", + domain: "", + entity_id: "", +}; diff --git a/src/panels/config/js/trigger/device.js b/src/panels/config/js/trigger/device.js index f5d857ab57..ee4ef4d982 100644 --- a/src/panels/config/js/trigger/device.js +++ b/src/panels/config/js/trigger/device.js @@ -2,6 +2,7 @@ import { h, Component } from "preact"; import "../../../../components/device/ha-device-picker"; import "../../../../components/device/ha-device-trigger-picker"; +import "../../../../components/device/ha-device-automation-picker"; import { onChangeEvent } from "../../../../common/preact/event"; diff --git a/src/translations/en.json b/src/translations/en.json index 1d94f08219..58ffab5625 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -794,6 +794,9 @@ "unsupported_condition": "Unsupported condition: {condition}", "type_select": "Condition type", "type": { + "device": { + "label": "Device" + }, "state": { "label": "[%key:ui::panel::config::automation::editor::triggers::type::state::label%]", "state": "[%key:ui::panel::config::automation::editor::triggers::type::state::label%]"