mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 17:26:42 +00:00
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:
parent
a7fdbc069b
commit
f43abb5a9d
119
src/components/device/ha-device-picker.ts
Normal file
119
src/components/device/ha-device-picker.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
166
src/components/device/ha-device-trigger-picker.ts
Normal file
166
src/components/device/ha-device-trigger-picker.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
57
src/data/device_automation.ts
Normal file
57
src/data/device_automation.ts
Normal 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]) : ""
|
||||||
|
);
|
52
src/panels/config/js/trigger/device.js
Normal file
52
src/panels/config/js/trigger/device.js
Normal 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: "",
|
||||||
|
};
|
@ -4,6 +4,7 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
|
|||||||
import "@polymer/paper-item/paper-item";
|
import "@polymer/paper-item/paper-item";
|
||||||
import "@polymer/paper-listbox/paper-listbox";
|
import "@polymer/paper-listbox/paper-listbox";
|
||||||
|
|
||||||
|
import DeviceTrigger from "./device";
|
||||||
import EventTrigger from "./event";
|
import EventTrigger from "./event";
|
||||||
import GeolocationTrigger from "./geo_location";
|
import GeolocationTrigger from "./geo_location";
|
||||||
import HassTrigger from "./homeassistant";
|
import HassTrigger from "./homeassistant";
|
||||||
@ -18,6 +19,7 @@ import WebhookTrigger from "./webhook";
|
|||||||
import ZoneTrigger from "./zone";
|
import ZoneTrigger from "./zone";
|
||||||
|
|
||||||
const TYPES = {
|
const TYPES = {
|
||||||
|
device: DeviceTrigger,
|
||||||
event: EventTrigger,
|
event: EventTrigger,
|
||||||
state: StateTrigger,
|
state: StateTrigger,
|
||||||
geo_location: GeolocationTrigger,
|
geo_location: GeolocationTrigger,
|
||||||
|
@ -691,6 +691,9 @@
|
|||||||
"unsupported_platform": "Unsupported platform: {platform}",
|
"unsupported_platform": "Unsupported platform: {platform}",
|
||||||
"type_select": "Trigger type",
|
"type_select": "Trigger type",
|
||||||
"type": {
|
"type": {
|
||||||
|
"device": {
|
||||||
|
"label": "Device"
|
||||||
|
},
|
||||||
"event": {
|
"event": {
|
||||||
"label": "Event",
|
"label": "Event",
|
||||||
"event_type": "Event type",
|
"event_type": "Event type",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user