Add Thingtalk automation generation (#4216)

* thingtalk

* works

* Add device_class support and get placeholders from api

* Update
This commit is contained in:
Bram Kragten 2019-11-14 13:22:44 +01:00 committed by Paulus Schoutsen
parent d0b9c09f8f
commit 0dab5828fb
11 changed files with 793 additions and 20 deletions

21
src/common/util/patch.ts Normal file
View File

@ -0,0 +1,21 @@
export const applyPatch = (data, path, value) => {
if (path.length === 1) {
data[path[0]] = value;
} else {
if (!data[path[0]]) {
data[path[0]] = {};
}
return applyPatch(data[path[0]], path.slice(1), value);
}
};
export const getPath = (data, path) => {
if (path.length === 1) {
return data[path[0]];
} else {
if (data[path[0]] === undefined) {
return undefined;
}
return getPath(data[path[0]], path.slice(1));
}
};

View File

@ -34,6 +34,7 @@ import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import { computeDomain } from "../../common/entity/compute_domain";
interface Device {
name: string;
@ -64,20 +65,45 @@ const rowRenderer = (root: HTMLElement, _owner, model: { item: Device }) => {
};
@customElement("ha-device-picker")
class HaDevicePicker extends SubscribeMixin(LitElement) {
export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property() public hass!: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property() public devices?: DeviceRegistryEntry[];
@property() public areas?: AreaRegistryEntry[];
@property() public entities?: EntityRegistryEntry[];
@property({ type: Boolean }) private _opened?: boolean;
/**
* Show only devices with entities from specific domains.
* @type {Array}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show no devices with entities of these domains.
* @type {Array}
* @attr exclude-domains
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
/**
* Show only deviced with entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
@property({ type: Boolean })
private _opened?: boolean;
private _getDevices = memoizeOne(
(
devices: DeviceRegistryEntry[],
areas: AreaRegistryEntry[],
entities: EntityRegistryEntry[]
entities: EntityRegistryEntry[],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"]
): Device[] => {
if (!devices.length) {
return [];
@ -99,7 +125,53 @@ class HaDevicePicker extends SubscribeMixin(LitElement) {
areaLookup[area.area_id] = area;
}
const outputDevices = devices.map((device) => {
let inputDevices = [...devices];
if (includeDomains) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
});
}
if (excludeDomains) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return true;
}
return entities.every(
(entity) =>
!excludeDomains.includes(computeDomain(entity.entity_id))
);
});
}
if (includeDeviceClasses) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
});
}
const outputDevices = inputDevices.map((device) => {
return {
id: device.id,
name: computeDeviceName(
@ -135,7 +207,14 @@ class HaDevicePicker extends SubscribeMixin(LitElement) {
if (!this.devices || !this.areas || !this.entities) {
return;
}
const devices = this._getDevices(this.devices, this.areas, this.entities);
const devices = this._getDevices(
this.devices,
this.areas,
this.entities,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses
);
return html`
<vaadin-combo-box-light
item-value-path="id"
@ -148,7 +227,9 @@ class HaDevicePicker extends SubscribeMixin(LitElement) {
@value-changed=${this._deviceChanged}
>
<paper-input
.label=${this.label}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.device-picker.device")
: this.label}
class="input"
autocapitalize="none"
autocomplete="off"

View File

@ -21,6 +21,7 @@ import { HomeAssistant } from "../../types";
import { HassEntity } from "home-assistant-js-websocket";
import { PolymerChangedEvent } from "../../polymer-types";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
@ -62,7 +63,7 @@ class HaEntityPicker extends LitElement {
@property() public value?: string;
/**
* Show entities from specific domains.
* @type {string}
* @type {Array}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
@ -74,6 +75,13 @@ class HaEntityPicker extends LitElement {
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
/**
* Show only entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Boolean }) private _opened?: boolean;
@property() private _hass?: HomeAssistant;
@ -83,7 +91,8 @@ class HaEntityPicker extends LitElement {
hass: this["hass"],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
entityFilter: this["entityFilter"]
entityFilter: this["entityFilter"],
includeDeviceClasses: this["includeDeviceClasses"]
) => {
let states: HassEntity[] = [];
@ -94,18 +103,28 @@ class HaEntityPicker extends LitElement {
if (includeDomains) {
entityIds = entityIds.filter((eid) =>
includeDomains.includes(eid.substr(0, eid.indexOf(".")))
includeDomains.includes(computeDomain(eid))
);
}
if (excludeDomains) {
entityIds = entityIds.filter(
(eid) => !excludeDomains.includes(eid.substr(0, eid.indexOf(".")))
(eid) => !excludeDomains.includes(computeDomain(eid))
);
}
states = entityIds.sort().map((key) => hass!.states[key]);
if (includeDeviceClasses) {
states = states.filter(
(stateObj) =>
// We always want to include the entity of the current value
stateObj.entity_id === this.value ||
(stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class))
);
}
if (entityFilter) {
states = states.filter(
(stateObj) =>
@ -113,6 +132,7 @@ class HaEntityPicker extends LitElement {
stateObj.entity_id === this.value || entityFilter!(stateObj)
);
}
return states;
}
);
@ -130,7 +150,8 @@ class HaEntityPicker extends LitElement {
this._hass,
this.includeDomains,
this.excludeDomains,
this.entityFilter
this.entityFilter,
this.includeDeviceClasses
);
return html`

View File

@ -1,5 +1,7 @@
import { HomeAssistant } from "../types";
import { EntityFilter } from "../common/entity/entity_filter";
import { AutomationConfig } from "./automation";
import { PlaceholderContainer } from "../panels/config/automation/thingtalk/dialog-thingtalk";
interface CloudStatusBase {
logged_in: boolean;
@ -63,6 +65,11 @@ export interface CloudWebhook {
managed?: boolean;
}
export interface ThingTalkConversion {
config: Partial<AutomationConfig>;
placeholders: PlaceholderContainer;
}
export const fetchCloudStatus = (hass: HomeAssistant) =>
hass.callWS<CloudStatus>({ type: "cloud/status" });
@ -91,6 +98,9 @@ export const disconnectCloudRemote = (hass: HomeAssistant) =>
export const fetchCloudSubscriptionInfo = (hass: HomeAssistant) =>
hass.callWS<SubscriptionInfo>({ type: "cloud/subscription" });
export const convertThingTalk = (hass: HomeAssistant, query: string) =>
hass.callWS<ThingTalkConversion>({ type: "cloud/thingtalk/convert", query });
export const updateCloudPref = (
hass: HomeAssistant,
prefs: {

View File

@ -170,7 +170,8 @@ export class HaAutomationEditor extends LitElement {
}
if (changedProps.has("creatingNew") && this.creatingNew && this.hass) {
this._dirty = false;
const initData = getAutomationEditorInitData();
this._dirty = initData ? true : false;
this._config = {
alias: this.hass.localize(
"ui.panel.config.automation.editor.default_name"
@ -179,7 +180,7 @@ export class HaAutomationEditor extends LitElement {
trigger: [{ platform: "state" }],
condition: [],
action: [{ service: "" }],
...getAutomationEditorInitData(),
...initData,
};
}

View File

@ -23,9 +23,15 @@ import { computeStateName } from "../../../common/entity/compute_state_name";
import { computeRTL } from "../../../common/util/compute_rtl";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { AutomationEntity } from "../../../data/automation";
import {
AutomationEntity,
showAutomationEditor,
AutomationConfig,
} from "../../../data/automation";
import format_date_time from "../../../common/datetime/format_date_time";
import { fireEvent } from "../../../common/dom/fire_event";
import { showThingtalkDialog } from "./show-dialog-thingtalk";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
@customElement("ha-automation-picker")
class HaAutomationPicker extends LitElement {
@ -141,8 +147,7 @@ class HaAutomationPicker extends LitElement {
)}
</ha-card>
</ha-config-section>
<a href="/config/automation/new">
<div>
<ha-fab
slot="fab"
?is-wide=${this.isWide}
@ -151,8 +156,9 @@ class HaAutomationPicker extends LitElement {
"ui.panel.config.automation.picker.add_automation"
)}
?rtl=${computeRTL(this.hass)}
></ha-fab
></a>
@click=${this._createNew}
></ha-fab>
</div>
</hass-subpage>
`;
}
@ -162,6 +168,17 @@ class HaAutomationPicker extends LitElement {
fireEvent(this, "hass-more-info", { entityId });
}
private _createNew() {
if (!isComponentLoaded(this.hass, "cloud")) {
showAutomationEditor(this);
return;
}
showThingtalkDialog(this, {
callback: (config: Partial<AutomationConfig> | undefined) =>
showAutomationEditor(this, config),
});
}
static get styles(): CSSResultArray {
return [
haStyle,
@ -198,6 +215,7 @@ class HaAutomationPicker extends LitElement {
bottom: 16px;
right: 16px;
z-index: 1;
cursor: pointer;
}
ha-fab[is-wide] {

View File

@ -0,0 +1,20 @@
import { fireEvent } from "../../../common/dom/fire_event";
import { AutomationConfig } from "../../../data/automation";
export interface ThingtalkDialogParams {
callback: (config: Partial<AutomationConfig> | undefined) => void;
}
export const loadThingtalkDialog = () =>
import(/* webpackChunkName: "thingtalk-dialog" */ "./thingtalk/dialog-thingtalk");
export const showThingtalkDialog = (
element: HTMLElement,
dialogParams: ThingtalkDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "ha-dialog-thinktalk",
dialogImport: loadThingtalkDialog,
dialogParams,
});
};

View File

@ -0,0 +1,259 @@
import {
LitElement,
html,
css,
CSSResult,
TemplateResult,
property,
customElement,
query,
} from "lit-element";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-spinner/paper-spinner";
import "@material/mwc-button";
import "../../../../components/dialog/ha-paper-dialog";
import "./ha-thingtalk-placeholders";
import { ThingtalkDialogParams } from "../show-dialog-thingtalk";
import { PolymerChangedEvent } from "../../../../polymer-types";
import { haStyleDialog, haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
// tslint:disable-next-line
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { AutomationConfig } from "../../../../data/automation";
// tslint:disable-next-line
import { PlaceholderValues } from "./ha-thingtalk-placeholders";
import { convertThingTalk } from "../../../../data/cloud";
export interface Placeholder {
index: number;
fields: string[];
domains: string[];
device_classes?: string[];
}
export interface PlaceholderContainer {
[key: string]: Placeholder[];
}
@customElement("ha-dialog-thinktalk")
class DialogThingtalk extends LitElement {
@property() public hass!: HomeAssistant;
@property() private _error?: string;
@property() private _params?: ThingtalkDialogParams;
@property() private _submitting: boolean = false;
@property() private _opened = false;
@property() private _placeholders?: PlaceholderContainer;
@query("#input") private _input?: PaperInputElement;
private _value!: string;
private _config!: Partial<AutomationConfig>;
public showDialog(params: ThingtalkDialogParams): void {
this._params = params;
this._error = undefined;
this._opened = true;
}
protected render(): TemplateResult | void {
if (!this._params) {
return html``;
}
if (this._placeholders) {
return html`
<ha-thingtalk-placeholders
.hass=${this.hass}
.placeholders=${this._placeholders}
.opened=${this._opened}
.skip=${() => this._skip()}
@opened-changed=${this._openedChanged}
@placeholders-filled=${this._handlePlaceholders}
>
</ha-thingtalk-placeholders>
`;
}
return html`
<ha-paper-dialog
with-backdrop
.opened=${this._opened}
@opened-changed=${this._openedChanged}
>
<h2>Create a new automation</h2>
<paper-dialog-scrollable>
${this._error
? html`
<div class="error">${this._error}</div>
`
: ""}
Type below what this automation should do, and we will try to convert
it into a Home Assistant automation. (only English is supported for
now)<br /><br />
For example:
<ul @click=${this._handleExampleClick}>
<li>
<button class="link">
Turn off the lights when I leave home
</button>
</li>
<li>
<button class="link">
Turn on the lights when the sun is set
</button>
</li>
<li>
<button class="link">
Notify me if the door opens and I am not at home
</button>
</li>
<li>
<button class="link">
Turn the light on when motion is detected
</button>
</li>
</ul>
<paper-input
id="input"
label="What should this automation do?"
autofocus
@keyup=${this._handleKeyUp}
></paper-input>
<a
href="https://almond.stanford.edu/"
target="_blank"
class="attribution"
>Powered by Almond</a
>
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
<mwc-button class="left" @click="${this._skip}">
Skip
</mwc-button>
<mwc-button @click="${this._generate}" .disabled=${this._submitting}>
<paper-spinner
?active="${this._submitting}"
alt="Creating your automation..."
></paper-spinner>
Create automation
</mwc-button>
</div>
</ha-paper-dialog>
`;
}
private async _generate() {
this._value = this._input!.value as string;
if (!this._value) {
this._error = "Enter a command or tap skip.";
return;
}
this._submitting = true;
let config: Partial<AutomationConfig>;
let placeholders: PlaceholderContainer;
try {
const result = await convertThingTalk(this.hass, this._value);
config = result.config;
placeholders = result.placeholders;
} catch (err) {
this._error = err.message;
this._submitting = false;
return;
}
this._submitting = false;
if (!Object.keys(config).length) {
this._error = "We couldn't create an automation for that (yet?).";
} else if (Object.keys(placeholders).length) {
this._config = config;
this._placeholders = placeholders;
} else {
this._sendConfig(this._value, config);
}
}
private _handlePlaceholders(ev: CustomEvent) {
const placeholderValues = ev.detail.value as PlaceholderValues;
Object.entries(placeholderValues).forEach(([type, values]) => {
Object.entries(values).forEach(([index, placeholder]) => {
Object.entries(placeholder).forEach(([field, value]) => {
this._config[type][index][field] = value;
});
});
});
this._sendConfig(this._value, this._config);
}
private _sendConfig(input, config) {
this._params!.callback({ alias: input, ...config });
this._closeDialog();
}
private _skip() {
this._params!.callback(undefined);
this._closeDialog();
}
private _closeDialog() {
this._placeholders = undefined;
if (this._input) {
this._input.value = null;
}
this._opened = false;
}
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
if (!ev.detail.value) {
this._closeDialog();
}
}
private _handleKeyUp(ev: KeyboardEvent) {
if (ev.keyCode === 13) {
this._generate();
}
}
private _handleExampleClick(ev: Event) {
this._input!.value = (ev.target as HTMLAnchorElement).innerText;
}
static get styles(): CSSResult[] {
return [
haStyle,
haStyleDialog,
css`
ha-paper-dialog {
max-width: 500px;
}
mwc-button.left {
margin-right: auto;
}
mwc-button paper-spinner {
width: 14px;
height: 14px;
margin-right: 20px;
}
paper-spinner {
display: none;
}
paper-spinner[active] {
display: block;
}
.error {
color: var(--google-red-500);
}
.attribution {
color: var(--secondary-text-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-dialog-thinktalk": DialogThingtalk;
}
}

View File

@ -0,0 +1,338 @@
import {
LitElement,
html,
TemplateResult,
property,
customElement,
css,
CSSResult,
query,
} from "lit-element";
import { HomeAssistant } from "../../../../types";
import { PolymerChangedEvent } from "../../../../polymer-types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { haStyleDialog } from "../../../../resources/styles";
import { PlaceholderContainer, Placeholder } from "./dialog-thingtalk";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { subscribeEntityRegistry } from "../../../../data/entity_registry";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { HassEntity } from "home-assistant-js-websocket";
import { HaDevicePicker } from "../../../../components/device/ha-device-picker";
import { getPath, applyPatch } from "../../../../common/util/patch";
declare global {
// for fire event
interface HASSDomEvents {
"placeholders-filled": { value: PlaceholderValues };
}
}
export interface PlaceholderValues {
[key: string]: { [index: number]: { [key: string]: string } };
}
interface DeviceEntitiesLookup {
[deviceId: string]: string[];
}
@customElement("ha-thingtalk-placeholders")
export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
@property() public hass!: HomeAssistant;
@property() public opened!: boolean;
public skip!: () => void;
@property() public placeholders!: PlaceholderContainer;
@property() private _error?: string;
private _deviceEntityLookup: DeviceEntitiesLookup = {};
private _manualEntities: PlaceholderValues = {};
@property() private _placeholderValues: PlaceholderValues = {};
@query("#device-entity-picker") private _deviceEntityPicker?: HaDevicePicker;
public hassSubscribe() {
return [
subscribeEntityRegistry(this.hass.connection, (entries) => {
for (const entity of entries) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in this._deviceEntityLookup)) {
this._deviceEntityLookup[entity.device_id] = [];
}
if (
!this._deviceEntityLookup[entity.device_id].includes(
entity.entity_id
)
) {
this._deviceEntityLookup[entity.device_id].push(entity.entity_id);
}
}
}),
];
}
protected render(): TemplateResult | void {
return html`
<ha-paper-dialog
with-backdrop
.opened=${this.opened}
@opened-changed="${this._openedChanged}"
>
<h2>Great! Now we need to link some devices.</h2>
<paper-dialog-scrollable>
${this._error
? html`
<div class="error">${this._error}</div>
`
: ""}
${Object.entries(this.placeholders).map(
([type, placeholders]) =>
html`
<h3>
${this.hass.localize(
`ui.panel.config.automation.editor.${type}s.name`
)}:
</h3>
${placeholders.map((placeholder) => {
if (placeholder.fields.includes("device_id")) {
return html`
<ha-device-picker
.type=${type}
.placeholder=${placeholder}
@change=${this._devicePicked}
.hass=${this.hass}
.includeDomains=${placeholder.domains}
.includeDeviceClasses=${placeholder.device_classes}
.label=${this._getLabel(
placeholder.domains,
placeholder.device_classes
)}
></ha-device-picker>
${(getPath(this._placeholderValues, [
type,
placeholder.index,
"device_id",
]) &&
placeholder.fields.includes("entity_id") &&
getPath(this._placeholderValues, [
type,
placeholder.index,
"entity_id",
]) === undefined) ||
getPath(this._manualEntities, [
type,
placeholder.index,
"manual",
]) === true
? html`
<ha-entity-picker
id="device-entity-picker"
.type=${type}
.placeholder=${placeholder}
@change=${this._entityPicked}
.includeDomains=${placeholder.domains}
.includeDeviceClasses=${placeholder.device_classes}
.hass=${this.hass}
.label=${this._getLabel(
placeholder.domains,
placeholder.device_classes
)}
.entityFilter=${(state: HassEntity) =>
this._deviceEntityLookup[
this._placeholderValues[type][
placeholder.index
].device_id
].includes(state.entity_id)}
></ha-entity-picker>
`
: ""}
`;
} else if (placeholder.fields.includes("entity_id")) {
return html`
<ha-entity-picker
.type=${type}
.placeholder=${placeholder}
@change=${this._entityPicked}
.includeDomains=${placeholder.domains}
.includeDeviceClasses=${placeholder.device_classes}
.hass=${this.hass}
.label=${this._getLabel(
placeholder.domains,
placeholder.device_classes
)}
></ha-entity-picker>
`;
}
return html`
<div class="error">
Unknown placeholder<br />
${placeholder.domains}<br />
${placeholder.fields.map(
(field) =>
html`
${field}<br />
`
)}
</div>
`;
})}
`
)}
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
<mwc-button class="left" @click="${this.skip}">
Skip
</mwc-button>
<mwc-button @click="${this._done}" .disabled=${!this._isDone}>
Create automation
</mwc-button>
</div>
</ha-paper-dialog>
`;
}
private get _isDone(): boolean {
return Object.entries(this.placeholders).every(([type, placeholders]) =>
placeholders.every((placeholder) =>
placeholder.fields.every(
(field) =>
getPath(this._placeholderValues, [
type,
placeholder.index,
field,
]) !== undefined
)
)
);
}
private _getLabel(domains: string[], deviceClasses?: string[]) {
return `${domains
.map((domain) => this.hass.localize(`domain.${domain}`))
.join(", ")}${
deviceClasses ? ` of type ${deviceClasses.join(", ")}` : ""
}`;
}
private _devicePicked(ev: Event): void {
const target = ev.target as any;
const placeholder = target.placeholder as Placeholder;
const value = target.value;
const type = target.type;
applyPatch(
this._placeholderValues,
[type, placeholder.index, "device_id"],
value
);
if (!placeholder.fields.includes("entity_id")) {
return;
}
if (value === "") {
delete this._placeholderValues[type][placeholder.index].entity_id;
if (this._deviceEntityPicker) {
this._deviceEntityPicker.value = undefined;
}
applyPatch(
this._manualEntities,
[type, placeholder.index, "manual"],
false
);
this.requestUpdate("_placeholderValues");
return;
}
const devEntities = this._deviceEntityLookup[value];
const entities = devEntities.filter((eid) => {
if (placeholder.device_classes) {
const stateObj = this.hass.states[eid];
if (!stateObj) {
return false;
}
return (
placeholder.domains.includes(computeDomain(eid)) &&
stateObj.attributes.device_class &&
placeholder.device_classes.includes(stateObj.attributes.device_class)
);
}
return placeholder.domains.includes(computeDomain(eid));
});
if (entities.length === 0) {
// Should not happen because we filter the device picker on domain
this._error = `No ${placeholder.domains
.map((domain) => this.hass.localize(`domain.${domain}`))
.join(", ")} entities found in this device.`;
} else if (entities.length === 1) {
applyPatch(
this._placeholderValues,
[type, placeholder.index, "entity_id"],
entities[0]
);
applyPatch(
this._manualEntities,
[type, placeholder.index, "manual"],
false
);
this.requestUpdate("_placeholderValues");
} else {
delete this._placeholderValues[type][placeholder.index].entity_id;
if (this._deviceEntityPicker) {
this._deviceEntityPicker.value = undefined;
}
applyPatch(
this._manualEntities,
[type, placeholder.index, "manual"],
true
);
this.requestUpdate("_placeholderValues");
}
}
private _entityPicked(ev: Event): void {
const target = ev.target as any;
const placeholder = target.placeholder as Placeholder;
const value = target.value;
const type = target.type;
applyPatch(
this._placeholderValues,
[type, placeholder.index, "entity_id"],
value
);
this.requestUpdate("_placeholderValues");
}
private _done(): void {
fireEvent(this, "placeholders-filled", { value: this._placeholderValues });
}
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
// The opened-changed event doesn't leave the shadowdom so we re-dispatch it
this.dispatchEvent(new CustomEvent(ev.type, ev));
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
ha-paper-dialog {
max-width: 500px;
}
mwc-button.left {
margin-right: auto;
}
paper-dialog-scrollable {
margin-top: 10px;
}
h3 {
margin: 10px 0 0 0;
font-weight: 500;
}
.error {
color: var(--google-red-500);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-thingtalk-placeholders": ThingTalkPlaceholders;
}
}

View File

@ -68,7 +68,7 @@ export default class DeviceActionEditor extends Component<
{extraFieldsData && (
<ha-form
data={Object.assign({}, ...extraFieldsData)}
onData-changed={this._extraFieldsChanged}
onvalue-changed={this._extraFieldsChanged}
schema={this.state.capabilities.extra_fields}
computeLabel={this._extraFieldsComputeLabelCallback(hass.localize)}
/>

View File

@ -531,7 +531,8 @@
},
"device-picker": {
"clear": "Clear",
"show_devices": "Show devices"
"show_devices": "Show devices",
"device": "Device"
},
"relative_time": {
"past": "{time} ago",
@ -782,6 +783,7 @@
"placeholder": "Optional description"
},
"triggers": {
"name": "Trigger",
"header": "Triggers",
"introduction": "Triggers are what starts the processing of an automation rule. It is possible to specify multiple triggers for the same rule. Once a trigger starts, Home Assistant will validate the conditions, if any, and call the action.",
"learn_more": "Learn more about triggers",
@ -872,6 +874,7 @@
}
},
"conditions": {
"name": "Condition",
"header": "Conditions",
"introduction": "Conditions are an optional part of an automation rule and can be used to prevent an action from happening when triggered. Conditions look very similar to triggers but are very different. A trigger will look at events happening in the system while a condition only looks at how the system looks right now. A trigger can observe that a switch is being turned on. A condition can only see if a switch is currently on or off.",
"learn_more": "Learn more about conditions",
@ -932,6 +935,7 @@
}
},
"actions": {
"name": "Action",
"header": "Actions",
"introduction": "The actions are what Home Assistant will do when the automation is triggered.",
"learn_more": "Learn more about actions",