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
This commit is contained in:
Erik Montnemery 2019-09-06 00:43:14 +02:00 committed by Paulus Schoutsen
parent 9205837b67
commit 011219b727
8 changed files with 350 additions and 168 deletions

View File

@ -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<T[]>;
private _createNoAutomation: (deviceId?: string) => T;
constructor(
localizeDeviceAutomation: HaDeviceAutomationPicker<
T
>["_localizeDeviceAutomation"],
fetchDeviceAutomations: HaDeviceAutomationPicker<
T
>["_fetchDeviceAutomations"],
createNoAutomation: HaDeviceAutomationPicker<T>["_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`
<ha-paper-dropdown-menu
.label=${this.label}
.value=${this.value
? this._localizeDeviceAutomation(this.hass, this.value)
: ""}
?disabled=${this._automations.length === 0}
>
<paper-listbox
slot="dropdown-content"
.selected=${this._key}
attr-for-selected="key"
@iron-select=${this._automationChanged}
>
<paper-item
key=${NO_AUTOMATION_KEY}
.automation=${this._createNoAutomation(this.deviceId)}
hidden
>
${this.NO_AUTOMATION_TEXT}
</paper-item>
<paper-item
key=${UNKNOWN_AUTOMATION_KEY}
.automation=${this.value}
hidden
>
${this.UNKNOWN_AUTOMATION_TEXT}
</paper-item>
${this._automations.map(
(automation, idx) => html`
<paper-item
key=${`${this.deviceId}_${idx}`}
.automation=${automation}
>
${this._localizeDeviceAutomation(this.hass, automation)}
</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._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;
}
`;
}
}

View File

@ -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;
}
}

View File

@ -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<DeviceTrigger> {
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`
<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;
}
`;
}
}

View File

@ -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<DeviceTriggerList>({
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<DeviceCondition[]>({
type: "device_automation/condition/list",
device_id: deviceId,
});
export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) =>
hass.callWS<DeviceTrigger[]>({
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) : "<unknown>"
);
};
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) : "<unknown>"
);
};

View File

@ -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,

View File

@ -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 (
<div>
<ha-device-picker
value={device_id}
onChange={this.devicePicked}
hass={hass}
label="Device"
/>
<ha-device-condition-picker
value={condition}
deviceId={device_id}
onChange={this.deviceConditionPicked}
hass={hass}
label="Condition"
/>
</div>
);
}
}
DeviceCondition.defaultConfig = {
device_id: "",
domain: "",
entity_id: "",
};

View File

@ -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";

View File

@ -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%]"