Support device triggers in automation editor (#3514)

* Support device trigger in automation editor

* Fix review comments, improve usability.

* Lint

* Lint

* Improve styling, address review comments

* Fix support for unknown stored automation

* Fix

* Lint

* Lint

* Index trigger by key, not by object

* Fix no trigger case

* Fix typing

* Move trigger translations to backend

* Rename WS command to device_automation/trigger/list

* Tweak

* Update src/data/device_automation.ts

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>

* Address review comments

* Fix..

* Simplify ha-device-trigger-picker

* Fix changing device
This commit is contained in:
Erik Montnemery 2019-09-02 06:45:47 +02:00 committed by Paulus Schoutsen
parent a7fdbc069b
commit f43abb5a9d
6 changed files with 399 additions and 0 deletions

View File

@ -0,0 +1,119 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@polymer/paper-listbox/paper-listbox";
import memoizeOne from "memoize-one";
import {
LitElement,
TemplateResult,
html,
css,
CSSResult,
customElement,
property,
} from "lit-element";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../data/device_registry";
import { compare } from "../../common/string/compare";
@customElement("ha-device-picker")
class HaDevicePicker extends LitElement {
public hass?: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property() public devices?: DeviceRegistryEntry[];
private _unsubDevices?: UnsubscribeFunc;
private _sortedDevices = memoizeOne((devices?: DeviceRegistryEntry[]) => {
if (!devices || devices.length === 1) {
return devices || [];
}
const sorted = [...devices];
sorted.sort((a, b) => compare(a.name || "", b.name || ""));
return sorted;
});
public connectedCallback() {
super.connectedCallback();
this._unsubDevices = subscribeDeviceRegistry(
this.hass!.connection!,
(devices) => {
this.devices = devices;
}
);
}
public disconnectedCallback() {
super.disconnectedCallback();
if (this._unsubDevices) {
this._unsubDevices();
this._unsubDevices = undefined;
}
}
protected render(): TemplateResult | void {
return html`
<paper-dropdown-menu-light .label=${this.label}>
<paper-listbox
slot="dropdown-content"
.selected=${this._value}
attr-for-selected="data-device-id"
@iron-select=${this._deviceChanged}
>
<paper-item data-device-id="">
No device
</paper-item>
${this._sortedDevices(this.devices).map(
(device) => html`
<paper-item data-device-id=${device.id}>
${device.name_by_user || device.name}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
`;
}
private get _value() {
return this.value || "";
}
private _deviceChanged(ev) {
const newValue = ev.detail.item.dataset.deviceId;
if (newValue !== this._value) {
this.value = newValue;
setTimeout(() => {
fireEvent(this, "value-changed", { value: newValue });
fireEvent(this, "change");
}, 0);
}
}
static get styles(): CSSResult {
return css`
paper-dropdown-menu-light {
width: 100%;
}
paper-listbox {
min-width: 200px;
}
paper-item {
cursor: pointer;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-device-picker": HaDevicePicker;
}
}

View File

@ -0,0 +1,166 @@
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 {
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";
@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[] = [];
// 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) {
return NO_TRIGGER_KEY;
}
const idx = this._triggers.findIndex((trigger) =>
deviceAutomationTriggersEqual(trigger, this.value!)
);
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`
<ha-paper-dropdown-menu
.label=${this.label}
.value=${this.value
? localizeDeviceAutomationTrigger(this.hass, this.value)
: ""}
?disabled=${this._triggers.length === 0}
>
<paper-listbox
slot="dropdown-content"
.selected=${this._key}
attr-for-selected="key"
@iron-select=${this._triggerChanged}
>
<paper-item key=${NO_TRIGGER_KEY} .trigger=${this._noTrigger} hidden>
No triggers
</paper-item>
<paper-item key=${UNKNOWN_TRIGGER_KEY} .trigger=${this.value} hidden>
Unknown trigger
</paper-item>
${this._triggers.map(
(trigger, idx) => html`
<paper-item key=${`${this.deviceId}_${idx}`} .trigger=${trigger}>
${localizeDeviceAutomationTrigger(this.hass, trigger)}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
`;
}
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;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-device-trigger-picker": HaDeviceTriggerPicker;
}
}

View File

@ -0,0 +1,57 @@
import { HomeAssistant } from "../types";
import compute_state_name from "../common/entity/compute_state_name";
export interface DeviceTrigger {
platform: string;
device_id: string;
domain: string;
entity_id: string;
type?: string;
event?: string;
}
export interface DeviceTriggerList {
triggers: DeviceTrigger[];
}
export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) =>
hass
.callWS<DeviceTriggerList>({
type: "device_automation/trigger/list",
device_id: deviceId,
})
.then((response) => response.triggers);
export const deviceAutomationTriggersEqual = (
a: DeviceTrigger,
b: DeviceTrigger
) => {
if (typeof a !== typeof b) {
return false;
}
for (const property in a) {
if (!Object.is(a[property], b[property])) {
return false;
}
}
for (const property in b) {
if (!Object.is(a[property], b[property])) {
return false;
}
}
return true;
};
export const localizeDeviceAutomationTrigger = (
hass: HomeAssistant,
trigger: DeviceTrigger
) =>
hass.localize(
`component.${trigger.domain}.device_automation.trigger_type.${
trigger.type
}`,
"name",
trigger.entity_id ? compute_state_name(hass!.states[trigger.entity_id]) : ""
);

View File

@ -0,0 +1,52 @@
import { h, Component } from "preact";
import "../../../../components/device/ha-device-picker";
import "../../../../components/device/ha-device-trigger-picker";
import { onChangeEvent } from "../../../../common/preact/event";
export default class DeviceTrigger extends Component {
constructor() {
super();
this.onChange = onChangeEvent.bind(this, "trigger");
this.devicePicked = this.devicePicked.bind(this);
this.deviceTriggerPicked = this.deviceTriggerPicked.bind(this);
this.state.device_id = undefined;
}
devicePicked(ev) {
this.setState({ device_id: ev.target.value });
}
deviceTriggerPicked(ev) {
const deviceTrigger = ev.target.value;
this.props.onChange(this.props.index, (this.props.trigger = deviceTrigger));
}
/* eslint-disable camelcase */
render({ trigger, hass }, { device_id }) {
if (device_id === undefined) device_id = trigger.device_id;
return (
<div>
<ha-device-picker
value={device_id}
onChange={this.devicePicked}
hass={hass}
label="Device"
/>
<ha-device-trigger-picker
value={trigger}
deviceId={device_id}
onChange={this.deviceTriggerPicked}
hass={hass}
label="Trigger"
/>
</div>
);
}
}
DeviceTrigger.defaultConfig = {
device_id: "",
};

View File

@ -4,6 +4,7 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import DeviceTrigger from "./device";
import EventTrigger from "./event";
import GeolocationTrigger from "./geo_location";
import HassTrigger from "./homeassistant";
@ -18,6 +19,7 @@ import WebhookTrigger from "./webhook";
import ZoneTrigger from "./zone";
const TYPES = {
device: DeviceTrigger,
event: EventTrigger,
state: StateTrigger,
geo_location: GeolocationTrigger,

View File

@ -691,6 +691,9 @@
"unsupported_platform": "Unsupported platform: {platform}",
"type_select": "Trigger type",
"type": {
"device": {
"label": "Device"
},
"event": {
"label": "Event",
"event_type": "Event type",