Area/multiple devices and name support for thingtalk automations (#4272)
* WIP: Area/multiple devices and name support * Fix removing devices * Don't recalc entities for all devices every time * Use guards * Update ha-thingtalk-placeholders.ts
This commit is contained in:
parent
caa604d5ca
commit
93165c9111
412
src/components/device/ha-area-devices-picker.ts
Normal file
412
src/components/device/ha-area-devices-picker.ts
Normal file
@ -0,0 +1,412 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import "./ha-devices-picker";
|
||||
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../data/device_registry";
|
||||
import { compare } from "../../common/string/compare";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../../data/area_registry";
|
||||
import { DeviceEntityLookup } from "../../panels/config/devices/ha-devices-data-table";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../data/entity_registry";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
|
||||
interface DevicesByArea {
|
||||
[areaId: string]: AreaDevices;
|
||||
}
|
||||
|
||||
interface AreaDevices {
|
||||
id?: string;
|
||||
name: string;
|
||||
devices: string[];
|
||||
}
|
||||
|
||||
const rowRenderer = (
|
||||
root: HTMLElement,
|
||||
_owner,
|
||||
model: { item: AreaDevices }
|
||||
) => {
|
||||
if (!root.firstElementChild) {
|
||||
root.innerHTML = `
|
||||
<style>
|
||||
paper-item {
|
||||
width: 100%;
|
||||
margin: -10px 0;
|
||||
padding: 0;
|
||||
}
|
||||
paper-icon-button {
|
||||
float: right;
|
||||
}
|
||||
.devices {
|
||||
display: none;
|
||||
}
|
||||
.devices.visible {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<paper-item>
|
||||
<paper-item-body two-line="">
|
||||
<div class='name'>[[item.name]]</div>
|
||||
<div secondary>[[item.devices.length]] devices</div>
|
||||
</paper-item-body>
|
||||
</paper-item>
|
||||
`;
|
||||
}
|
||||
root.querySelector(".name")!.textContent = model.item.name!;
|
||||
root.querySelector(
|
||||
"[secondary]"
|
||||
)!.textContent = `${model.item.devices.length.toString()} devices`;
|
||||
};
|
||||
|
||||
@customElement("ha-area-devices-picker")
|
||||
export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public label?: string;
|
||||
@property() public value?: string;
|
||||
@property() public area?: string;
|
||||
@property() public devices?: string[];
|
||||
/**
|
||||
* 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;
|
||||
@property() private _areaPicker = true;
|
||||
@property() private _devices?: DeviceRegistryEntry[];
|
||||
@property() private _areas?: AreaRegistryEntry[];
|
||||
@property() private _entities?: EntityRegistryEntry[];
|
||||
private _selectedDevices: string[] = [];
|
||||
private _filteredDevices: DeviceRegistryEntry[] = [];
|
||||
|
||||
private _getDevices = memoizeOne(
|
||||
(
|
||||
devices: DeviceRegistryEntry[],
|
||||
areas: AreaRegistryEntry[],
|
||||
entities: EntityRegistryEntry[],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"]
|
||||
): AreaDevices[] => {
|
||||
if (!devices.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||
for (const entity of entities) {
|
||||
if (!entity.device_id) {
|
||||
continue;
|
||||
}
|
||||
if (!(entity.device_id in deviceEntityLookup)) {
|
||||
deviceEntityLookup[entity.device_id] = [];
|
||||
}
|
||||
deviceEntityLookup[entity.device_id].push(entity);
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this._filteredDevices = inputDevices;
|
||||
|
||||
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
|
||||
for (const area of areas) {
|
||||
areaLookup[area.area_id] = area;
|
||||
}
|
||||
|
||||
const devicesByArea: DevicesByArea = {};
|
||||
|
||||
for (const device of inputDevices) {
|
||||
const areaId = device.area_id;
|
||||
if (areaId) {
|
||||
if (!(areaId in devicesByArea)) {
|
||||
devicesByArea[areaId] = {
|
||||
id: areaId,
|
||||
name: areaLookup[areaId].name,
|
||||
devices: [],
|
||||
};
|
||||
}
|
||||
devicesByArea[areaId].devices.push(device.id);
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = Object.keys(devicesByArea)
|
||||
.sort((a, b) =>
|
||||
compare(devicesByArea[a].name || "", devicesByArea[b].name || "")
|
||||
)
|
||||
.map((key) => devicesByArea[key]);
|
||||
|
||||
return sorted;
|
||||
}
|
||||
);
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||
this._devices = devices;
|
||||
}),
|
||||
subscribeAreaRegistry(this.hass.connection!, (areas) => {
|
||||
this._areas = areas;
|
||||
}),
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
this._entities = entities;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("area") && this.area) {
|
||||
this._areaPicker = true;
|
||||
this.value = this.area;
|
||||
} else if (changedProps.has("devices") && this.devices) {
|
||||
this._areaPicker = false;
|
||||
const filteredDeviceIds = this._filteredDevices.map(
|
||||
(device) => device.id
|
||||
);
|
||||
const selectedDevices = this.devices.filter((device) =>
|
||||
filteredDeviceIds.includes(device)
|
||||
);
|
||||
this._setValue(selectedDevices);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this._devices || !this._areas || !this._entities) {
|
||||
return;
|
||||
}
|
||||
const areas = this._getDevices(
|
||||
this._devices,
|
||||
this._areas,
|
||||
this._entities,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses
|
||||
);
|
||||
if (!this._areaPicker || areas.length === 0) {
|
||||
return html`
|
||||
<ha-devices-picker
|
||||
@value-changed=${this._devicesPicked}
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.value=${this._selectedDevices}
|
||||
.pickDeviceLabel=${`Add ${this.label} device`}
|
||||
.pickedDeviceLabel=${`${this.label} device`}
|
||||
></ha-devices-picker>
|
||||
${areas.length > 0
|
||||
? html`
|
||||
<mwc-button @click=${this._switchPicker}
|
||||
>Choose an area</mwc-button
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<vaadin-combo-box-light
|
||||
item-value-path="id"
|
||||
item-id-path="id"
|
||||
item-label-path="name"
|
||||
.items=${areas}
|
||||
.value=${this._value}
|
||||
.renderer=${rowRenderer}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._areaPicked}
|
||||
>
|
||||
<paper-input
|
||||
.label=${this.label === undefined && this.hass
|
||||
? this.hass.localize("ui.components.device-picker.device")
|
||||
: `${this.label} in area`}
|
||||
class="input"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
>
|
||||
${this.value
|
||||
? html`
|
||||
<paper-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.device-picker.clear"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="clear-button"
|
||||
icon="hass:close"
|
||||
@click=${this._clearValue}
|
||||
no-ripple
|
||||
>
|
||||
Clear
|
||||
</paper-icon-button>
|
||||
`
|
||||
: ""}
|
||||
${areas.length > 0
|
||||
? html`
|
||||
<paper-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.device-picker.show_devices"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="toggle-button"
|
||||
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
|
||||
>
|
||||
Toggle
|
||||
</paper-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</paper-input>
|
||||
</vaadin-combo-box-light>
|
||||
<mwc-button @click=${this._switchPicker}
|
||||
>Choose individual devices</mwc-button
|
||||
>
|
||||
`;
|
||||
}
|
||||
|
||||
private _clearValue(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._setValue([]);
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || [];
|
||||
}
|
||||
|
||||
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
|
||||
this._opened = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _switchPicker() {
|
||||
this._areaPicker = !this._areaPicker;
|
||||
}
|
||||
|
||||
private async _areaPicked(ev: PolymerChangedEvent<string>) {
|
||||
const value = ev.detail.value;
|
||||
let selectedDevices = [];
|
||||
const target = ev.target as any;
|
||||
if (target.selectedItem) {
|
||||
selectedDevices = target.selectedItem.devices;
|
||||
}
|
||||
|
||||
if (value !== this._value || this._selectedDevices !== selectedDevices) {
|
||||
this._setValue(selectedDevices, value);
|
||||
}
|
||||
}
|
||||
|
||||
private _devicesPicked(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const selectedDevices = ev.detail.value;
|
||||
this._setValue(selectedDevices);
|
||||
}
|
||||
|
||||
private _setValue(selectedDevices: string[], value = "") {
|
||||
this.value = value;
|
||||
this._selectedDevices = selectedDevices;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value: selectedDevices });
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
paper-input > paper-icon-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 2px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-area-devices-picker": HaAreaDevicesPicker;
|
||||
}
|
||||
}
|
127
src/components/device/ha-devices-picker.ts
Normal file
127
src/components/device/ha-devices-picker.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
property,
|
||||
html,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-icon-button/paper-icon-button-light";
|
||||
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
import "./ha-device-picker";
|
||||
|
||||
@customElement("ha-devices-picker")
|
||||
class HaDevicesPicker extends LitElement {
|
||||
@property() public hass?: HomeAssistant;
|
||||
@property() public value?: string[];
|
||||
/**
|
||||
* Show entities from specific domains.
|
||||
* @type {string}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
/**
|
||||
* Show no entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
@property({ attribute: "picked-device-label" })
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
public pickedDeviceLabel?: string;
|
||||
@property({ attribute: "pick-device-label" }) public pickDeviceLabel?: string;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentDevices = this._currentDevices;
|
||||
return html`
|
||||
${currentDevices.map(
|
||||
(entityId) => html`
|
||||
<div>
|
||||
<ha-device-picker
|
||||
allow-custom-entity
|
||||
.curValue=${entityId}
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.value=${entityId}
|
||||
.label=${this.pickedDeviceLabel}
|
||||
@value-changed=${this._deviceChanged}
|
||||
></ha-device-picker>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<div>
|
||||
<ha-device-picker
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.label=${this.pickDeviceLabel}
|
||||
@value-changed=${this._addDevice}
|
||||
></ha-device-picker>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _currentDevices() {
|
||||
return this.value || [];
|
||||
}
|
||||
|
||||
private async _updateDevices(devices) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: devices,
|
||||
});
|
||||
|
||||
this.value = devices;
|
||||
}
|
||||
|
||||
private _deviceChanged(event: PolymerChangedEvent<string>) {
|
||||
event.stopPropagation();
|
||||
const curValue = (event.currentTarget as any).curValue;
|
||||
const newValue = event.detail.value;
|
||||
if (newValue === curValue || newValue !== "") {
|
||||
return;
|
||||
}
|
||||
if (newValue === "") {
|
||||
this._updateDevices(
|
||||
this._currentDevices.filter((dev) => dev !== curValue)
|
||||
);
|
||||
} else {
|
||||
this._updateDevices(
|
||||
this._currentDevices.map((dev) => (dev === curValue ? newValue : dev))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async _addDevice(event: PolymerChangedEvent<string>) {
|
||||
event.stopPropagation();
|
||||
const toAdd = event.detail.value;
|
||||
(event.currentTarget as any).value = "";
|
||||
if (!toAdd) {
|
||||
return;
|
||||
}
|
||||
const currentDevices = this._currentDevices;
|
||||
if (currentDevices.includes(toAdd)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateDevices([...currentDevices, toAdd]);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-devices-picker": HaDevicesPicker;
|
||||
}
|
||||
}
|
@ -27,6 +27,7 @@ import { PlaceholderValues } from "./ha-thingtalk-placeholders";
|
||||
import { convertThingTalk } from "../../../../data/cloud";
|
||||
|
||||
export interface Placeholder {
|
||||
name: string;
|
||||
index: number;
|
||||
fields: string[];
|
||||
domains: string[];
|
||||
@ -177,8 +178,21 @@ class DialogThingtalk extends LitElement {
|
||||
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;
|
||||
const devices = Object.values(placeholder);
|
||||
if (devices.length === 1) {
|
||||
Object.entries(devices[0]).forEach(([field, value]) => {
|
||||
this._config[type][index][field] = value;
|
||||
return;
|
||||
});
|
||||
}
|
||||
const automation = { ...this._config[type][index] };
|
||||
delete this._config[type][index];
|
||||
devices.forEach((fields) => {
|
||||
const newAutomation = { ...automation };
|
||||
Object.entries(fields).forEach(([field, value]) => {
|
||||
newAutomation[field] = value;
|
||||
});
|
||||
this._config[type].push(newAutomation);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -6,8 +6,11 @@ import {
|
||||
customElement,
|
||||
css,
|
||||
CSSResult,
|
||||
query,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
|
||||
import "../../../../components/device/ha-area-devices-picker";
|
||||
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { PolymerChangedEvent } from "../../../../polymer-types";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
@ -17,8 +20,15 @@ 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";
|
||||
import {
|
||||
subscribeAreaRegistry,
|
||||
AreaRegistryEntry,
|
||||
} from "../../../../data/area_registry";
|
||||
import {
|
||||
subscribeDeviceRegistry,
|
||||
DeviceRegistryEntry,
|
||||
} from "../../../../data/device_registry";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@ -28,7 +38,23 @@ declare global {
|
||||
}
|
||||
|
||||
export interface PlaceholderValues {
|
||||
[key: string]: { [index: number]: { [key: string]: string } };
|
||||
[key: string]: {
|
||||
[index: number]: {
|
||||
[index: number]: { device_id?: string; entity_id?: string };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ExtraInfo {
|
||||
[key: string]: {
|
||||
[index: number]: {
|
||||
[index: number]: {
|
||||
area_id?: string;
|
||||
device_ids?: string[];
|
||||
manualEntity: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface DeviceEntitiesLookup {
|
||||
@ -43,9 +69,11 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
|
||||
@property() public placeholders!: PlaceholderContainer;
|
||||
@property() private _error?: string;
|
||||
private _deviceEntityLookup: DeviceEntitiesLookup = {};
|
||||
private _manualEntities: PlaceholderValues = {};
|
||||
@property() private _extraInfo: ExtraInfo = {};
|
||||
@property() private _placeholderValues: PlaceholderValues = {};
|
||||
@query("#device-entity-picker") private _deviceEntityPicker?: HaDevicePicker;
|
||||
private _devices?: DeviceRegistryEntry[];
|
||||
private _areas?: AreaRegistryEntry[];
|
||||
private _search = false;
|
||||
|
||||
public hassSubscribe() {
|
||||
return [
|
||||
@ -66,12 +94,28 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
}),
|
||||
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||
this._devices = devices;
|
||||
this._searchNames();
|
||||
}),
|
||||
subscribeAreaRegistry(this.hass.connection!, (areas) => {
|
||||
this._areas = areas;
|
||||
this._searchNames();
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("placeholders")) {
|
||||
this._search = true;
|
||||
this._searchNames();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
<ha-paper-dialog
|
||||
modal
|
||||
with-backdrop
|
||||
.opened=${this.opened}
|
||||
@opened-changed="${this._openedChanged}"
|
||||
@ -93,55 +137,66 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
|
||||
</h3>
|
||||
${placeholders.map((placeholder) => {
|
||||
if (placeholder.fields.includes("device_id")) {
|
||||
const extraInfo = getPath(this._extraInfo, [
|
||||
type,
|
||||
placeholder.index,
|
||||
]);
|
||||
return html`
|
||||
<ha-device-picker
|
||||
<ha-area-devices-picker
|
||||
.type=${type}
|
||||
.placeholder=${placeholder}
|
||||
@change=${this._devicePicked}
|
||||
@value-changed=${this._devicePicked}
|
||||
.hass=${this.hass}
|
||||
.area=${extraInfo ? extraInfo.area_id : undefined}
|
||||
.devices=${extraInfo && extraInfo.device_ids
|
||||
? extraInfo.device_ids
|
||||
: undefined}
|
||||
.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
|
||||
></ha-area-devices-picker>
|
||||
${extraInfo && extraInfo.manualEntity
|
||||
? 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>
|
||||
<h3>
|
||||
One or more devices have more than one matching
|
||||
entity, please pick the one you want to use.
|
||||
</h3>
|
||||
${Object.keys(extraInfo.manualEntity).map(
|
||||
(idx) => html`
|
||||
<ha-entity-picker
|
||||
id="device-entity-picker"
|
||||
.type=${type}
|
||||
.placeholder=${placeholder}
|
||||
.index=${idx}
|
||||
@change=${this._entityPicked}
|
||||
.includeDomains=${placeholder.domains}
|
||||
.includeDeviceClasses=${placeholder.device_classes}
|
||||
.hass=${this.hass}
|
||||
.label=${`${this._getLabel(
|
||||
placeholder.domains,
|
||||
placeholder.device_classes
|
||||
)} of device ${this._getDeviceName(
|
||||
getPath(this._placeholderValues, [
|
||||
type,
|
||||
placeholder.index,
|
||||
idx,
|
||||
"device_id",
|
||||
])
|
||||
)}`}
|
||||
.entityFilter=${(state: HassEntity) => {
|
||||
const devId = this._placeholderValues[type][
|
||||
placeholder.index
|
||||
][idx].device_id;
|
||||
return this._deviceEntityLookup[
|
||||
devId
|
||||
].includes(state.entity_id);
|
||||
}}
|
||||
></ha-entity-picker>
|
||||
`
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
@ -189,17 +244,74 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private _getDeviceName(deviceId: string): string {
|
||||
if (!this._devices) {
|
||||
return "";
|
||||
}
|
||||
const foundDevice = this._devices.find((device) => device.id === deviceId);
|
||||
if (!foundDevice) {
|
||||
return "";
|
||||
}
|
||||
return foundDevice.name_by_user || foundDevice.name || "";
|
||||
}
|
||||
|
||||
private _searchNames() {
|
||||
if (!this._search || !this._areas || !this._devices) {
|
||||
return;
|
||||
}
|
||||
this._search = false;
|
||||
Object.entries(this.placeholders).forEach(([type, placeholders]) =>
|
||||
placeholders.forEach((placeholder) => {
|
||||
if (!placeholder.name) {
|
||||
return;
|
||||
}
|
||||
const name = placeholder.name;
|
||||
const foundArea = this._areas!.find((area) =>
|
||||
area.name.toLowerCase().includes(name)
|
||||
);
|
||||
if (foundArea) {
|
||||
applyPatch(
|
||||
this._extraInfo,
|
||||
[type, placeholder.index, "area_id"],
|
||||
foundArea.area_id
|
||||
);
|
||||
this.requestUpdate("_extraInfo");
|
||||
return;
|
||||
}
|
||||
const foundDevices = this._devices!.filter((device) => {
|
||||
const deviceName = device.name_by_user || device.name;
|
||||
if (!deviceName) {
|
||||
return false;
|
||||
}
|
||||
return deviceName.toLowerCase().includes(name);
|
||||
});
|
||||
if (foundDevices.length) {
|
||||
applyPatch(
|
||||
this._extraInfo,
|
||||
[type, placeholder.index, "device_ids"],
|
||||
foundDevices.map((device) => device.id)
|
||||
);
|
||||
this.requestUpdate("_extraInfo");
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
placeholder.fields.every((field) => {
|
||||
const entries: {
|
||||
[key: number]: { device_id?: string; entity_id?: string };
|
||||
} = getPath(this._placeholderValues, [type, placeholder.index]);
|
||||
if (!entries) {
|
||||
return false;
|
||||
}
|
||||
const values = Object.values(entries);
|
||||
return values.every(
|
||||
(entry) => entry[field] !== undefined && entry[field] !== ""
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -212,76 +324,115 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
|
||||
}`;
|
||||
}
|
||||
|
||||
private _devicePicked(ev: Event): void {
|
||||
private _devicePicked(ev: CustomEvent): void {
|
||||
const value: string[] = ev.detail.value;
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
|
||||
let oldValues = getPath(this._placeholderValues, [type, placeholder.index]);
|
||||
if (oldValues) {
|
||||
oldValues = Object.values(oldValues);
|
||||
}
|
||||
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
|
||||
);
|
||||
const oldExtraInfo = getPath(this._extraInfo, [type, placeholder.index]);
|
||||
|
||||
if (this._placeholderValues[type]) {
|
||||
delete this._placeholderValues[type][placeholder.index];
|
||||
}
|
||||
|
||||
if (this._extraInfo[type]) {
|
||||
delete this._extraInfo[type][placeholder.index];
|
||||
}
|
||||
|
||||
if (!value.length) {
|
||||
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;
|
||||
|
||||
value.forEach((deviceId, index) => {
|
||||
let oldIndex;
|
||||
if (oldValues) {
|
||||
const oldDevice = oldValues.find((oldVal, idx) => {
|
||||
oldIndex = idx;
|
||||
return oldVal.device_id === deviceId;
|
||||
});
|
||||
|
||||
if (oldDevice) {
|
||||
applyPatch(
|
||||
this._placeholderValues,
|
||||
[type, placeholder.index, index],
|
||||
oldDevice
|
||||
);
|
||||
if (oldExtraInfo) {
|
||||
applyPatch(
|
||||
this._extraInfo,
|
||||
[type, placeholder.index, index],
|
||||
oldExtraInfo[oldIndex]
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
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]
|
||||
[type, placeholder.index, index, "device_id"],
|
||||
deviceId
|
||||
);
|
||||
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;
|
||||
|
||||
if (!placeholder.fields.includes("entity_id")) {
|
||||
return;
|
||||
}
|
||||
applyPatch(
|
||||
this._manualEntities,
|
||||
[type, placeholder.index, "manual"],
|
||||
true
|
||||
);
|
||||
this.requestUpdate("_placeholderValues");
|
||||
}
|
||||
|
||||
const devEntities = this._deviceEntityLookup[deviceId];
|
||||
|
||||
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, index, "entity_id"],
|
||||
entities[0]
|
||||
);
|
||||
this.requestUpdate("_placeholderValues");
|
||||
} else {
|
||||
delete this._placeholderValues[type][placeholder.index][index]
|
||||
.entity_id;
|
||||
applyPatch(
|
||||
this._extraInfo,
|
||||
[type, placeholder.index, "manualEntity", index],
|
||||
true
|
||||
);
|
||||
this.requestUpdate("_placeholderValues");
|
||||
}
|
||||
});
|
||||
|
||||
fireEvent(
|
||||
this.shadowRoot!.querySelector("ha-paper-dialog")! as HTMLElement,
|
||||
"iron-resize"
|
||||
);
|
||||
}
|
||||
|
||||
private _entityPicked(ev: Event): void {
|
||||
@ -289,9 +440,10 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
|
||||
const placeholder = target.placeholder as Placeholder;
|
||||
const value = target.value;
|
||||
const type = target.type;
|
||||
const index = target.index || 0;
|
||||
applyPatch(
|
||||
this._placeholderValues,
|
||||
[type, placeholder.index, "entity_id"],
|
||||
[type, placeholder.index, index, "entity_id"],
|
||||
value
|
||||
);
|
||||
this.requestUpdate("_placeholderValues");
|
||||
|
Loading…
x
Reference in New Issue
Block a user