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-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,
|
||||
|
@ -691,6 +691,9 @@
|
||||
"unsupported_platform": "Unsupported platform: {platform}",
|
||||
"type_select": "Trigger type",
|
||||
"type": {
|
||||
"device": {
|
||||
"label": "Device"
|
||||
},
|
||||
"event": {
|
||||
"label": "Event",
|
||||
"event_type": "Event type",
|
||||
|
Loading…
x
Reference in New Issue
Block a user