diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts
new file mode 100644
index 0000000000..f9c64578a1
--- /dev/null
+++ b/src/components/device/ha-device-picker.ts
@@ -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`
+
+
+
+ No device
+
+ ${this._sortedDevices(this.devices).map(
+ (device) => html`
+
+ ${device.name_by_user || device.name}
+
+ `
+ )}
+
+
+ `;
+ }
+
+ 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;
+ }
+}
diff --git a/src/components/device/ha-device-trigger-picker.ts b/src/components/device/ha-device-trigger-picker.ts
new file mode 100644
index 0000000000..7ff3b6d48d
--- /dev/null
+++ b/src/components/device/ha-device-trigger-picker.ts
@@ -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`
+
+
+
+ No triggers
+
+
+ Unknown trigger
+
+ ${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;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-device-trigger-picker": HaDeviceTriggerPicker;
+ }
+}
diff --git a/src/data/device_automation.ts b/src/data/device_automation.ts
new file mode 100644
index 0000000000..d3c7370527
--- /dev/null
+++ b/src/data/device_automation.ts
@@ -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({
+ 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]) : ""
+ );
diff --git a/src/panels/config/js/trigger/device.js b/src/panels/config/js/trigger/device.js
new file mode 100644
index 0000000000..504eabdb7a
--- /dev/null
+++ b/src/panels/config/js/trigger/device.js
@@ -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 (
+
+
+
+
+ );
+ }
+}
+
+DeviceTrigger.defaultConfig = {
+ device_id: "",
+};
diff --git a/src/panels/config/js/trigger/trigger_edit.js b/src/panels/config/js/trigger/trigger_edit.js
index 443c094a3a..e0234ed550 100644
--- a/src/panels/config/js/trigger/trigger_edit.js
+++ b/src/panels/config/js/trigger/trigger_edit.js
@@ -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,
diff --git a/src/translations/en.json b/src/translations/en.json
index 9047b48fc8..32999f17db 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -691,6 +691,9 @@
"unsupported_platform": "Unsupported platform: {platform}",
"type_select": "Trigger type",
"type": {
+ "device": {
+ "label": "Device"
+ },
"event": {
"label": "Event",
"event_type": "Event type",