mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
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:
parent
9205837b67
commit
011219b727
195
src/components/device/ha-device-automation-picker.ts
Normal file
195
src/components/device/ha-device-automation-picker.ts
Normal 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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
35
src/components/device/ha-device-condition-picker.ts
Normal file
35
src/components/device/ha-device-condition-picker.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>"
|
||||
);
|
||||
};
|
||||
|
@ -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,
|
||||
|
57
src/panels/config/js/condition/device.js
Normal file
57
src/panels/config/js/condition/device.js
Normal 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: "",
|
||||
};
|
@ -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";
|
||||
|
||||
|
@ -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%]"
|
||||
|
Loading…
x
Reference in New Issue
Block a user