Add entity and device selectors (#7735)

This commit is contained in:
Bram Kragten 2020-11-20 13:26:03 +01:00 committed by GitHub
parent 46f5589530
commit f835810f0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 314 additions and 37 deletions

View File

@ -42,6 +42,10 @@ interface Device {
id: string;
}
export type HaDevicePickerDeviceFilterFunc = (
device: DeviceRegistryEntry
) => boolean;
const rowRenderer = (root: HTMLElement, _owner, model: { item: Device }) => {
if (!root.firstElementChild) {
root.innerHTML = `
@ -102,6 +106,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property({ type: Boolean })
private _opened?: boolean;
@ -112,7 +118,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
entities: EntityRegistryEntry[],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"]
includeDeviceClasses: this["includeDeviceClasses"],
deviceFilter: this["deviceFilter"]
): Device[] => {
if (!devices.length) {
return [];
@ -180,6 +187,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
});
}
if (deviceFilter) {
inputDevices = inputDevices.filter(
(device) =>
// We always want to include the device of the current value
device.id === this.value || deviceFilter!(device)
);
}
const outputDevices = inputDevices.map((device) => {
return {
id: device.id,
@ -224,7 +239,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
this.entities,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses
this.includeDeviceClasses,
this.deviceFilter
);
return html`
<vaadin-combo-box-light

View File

@ -13,7 +13,7 @@ import {
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { compare } from "../common/string/compare";
import { Blueprints, fetchBlueprints } from "../data/blueprint";
import { Blueprint, Blueprints, fetchBlueprints } from "../data/blueprint";
import { HomeAssistant } from "../types";
@customElement("ha-blueprint-picker")
@ -34,10 +34,12 @@ class HaBluePrintPicker extends LitElement {
if (!blueprints) {
return [];
}
const result = Object.entries(blueprints).map(([path, blueprint]) => ({
...blueprint.metadata,
path,
}));
const result = Object.entries(blueprints)
.filter(([_path, blueprint]) => !("error" in blueprint))
.map(([path, blueprint]) => ({
...(blueprint as Blueprint).metadata,
path,
}));
return result.sort((a, b) => compare(a.name, b.name));
});

View File

@ -0,0 +1,81 @@
import {
customElement,
html,
internalProperty,
LitElement,
property,
} from "lit-element";
import { HomeAssistant } from "../../types";
import "../device/ha-device-picker";
import { DeviceRegistryEntry } from "../../data/device_registry";
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import { DeviceSelector } from "../../data/selector";
@customElement("ha-selector-device")
export class HaDeviceSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: DeviceSelector;
@property() public value?: any;
@property() public label?: string;
@internalProperty() public _configEntries?: ConfigEntry[];
protected updated(changedProperties) {
if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector");
if (oldSelector !== this.selector && this.selector.device.integration) {
this._loadConfigEntries();
}
}
}
protected render() {
return html`<ha-device-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.deviceFilter=${(device) => this._filterDevices(device)}
allow-custom-entity
></ha-device-picker>`;
}
private _filterDevices(device: DeviceRegistryEntry): boolean {
if (
this.selector.device.manufacturer &&
device.manufacturer !== this.selector.device.manufacturer
) {
return false;
}
if (
this.selector.device.model &&
device.model !== this.selector.device.model
) {
return false;
}
if (this.selector.device.integration) {
if (
!this._configEntries?.some((entry) =>
device.config_entries.includes(entry.entry_id)
)
) {
return false;
}
}
return true;
}
private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter(
(entry) => entry.domain === this.selector.device.integration
);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-device": HaDeviceSelector;
}
}

View File

@ -0,0 +1,75 @@
import {
customElement,
html,
internalProperty,
LitElement,
property,
} from "lit-element";
import { HomeAssistant } from "../../types";
import "../entity/ha-entity-picker";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { subscribeEntityRegistry } from "../../data/entity_registry";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { EntitySelector } from "../../data/selector";
@customElement("ha-selector-entity")
export class HaEntitySelector extends SubscribeMixin(LitElement) {
@property() public hass!: HomeAssistant;
@property() public selector!: EntitySelector;
@internalProperty() private _entities?: Record<string, string>;
@property() public value?: any;
@property() public label?: string;
protected render() {
return html`<ha-entity-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.entityFilter=${(entity) => this._filterEntities(entity)}
allow-custom-entity
></ha-entity-picker>`;
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
const entityLookup = {};
for (const confEnt of entities) {
if (!confEnt.platform) {
continue;
}
entityLookup[confEnt.entity_id] = confEnt.platform;
}
this._entities = entityLookup;
}),
];
}
private _filterEntities(entity: HassEntity): boolean {
if (this.selector.entity.domain) {
if (computeStateDomain(entity) !== this.selector.entity.domain) {
return false;
}
}
if (this.selector.entity.integration) {
if (
!this._entities ||
this._entities[entity.entity_id] !== this.selector.entity.integration
) {
return false;
}
}
return true;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-entity": HaEntitySelector;
}
}

View File

@ -0,0 +1,48 @@
import { customElement, html, LitElement, property } from "lit-element";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { HomeAssistant } from "../../types";
import "./ha-selector-entity";
import "./ha-selector-device";
import { Selector } from "../../data/selector";
@customElement("ha-selector")
export class HaSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: Selector;
@property() public value?: any;
@property() public label?: string;
public focus() {
const input = this.shadowRoot!.getElementById("selector");
if (!input) {
return;
}
(input as HTMLElement).focus();
}
private get _type() {
return Object.keys(this.selector)[0];
}
protected render() {
return html`
${dynamicElement(`ha-selector-${this._type}`, {
hass: this.hass,
selector: this.selector,
value: this.value,
label: this.label,
id: "selector",
})}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector": HaSelector;
}
}

View File

@ -1,7 +1,9 @@
import { HomeAssistant } from "../types";
import { Selector } from "./selector";
export type Blueprints = Record<string, Blueprint>;
export type Blueprints = Record<string, BlueprintOrError>;
export type BlueprintOrError = Blueprint | { error: string };
export interface Blueprint {
metadata: BlueprintMetaData;
}
@ -9,10 +11,14 @@ export interface Blueprint {
export interface BlueprintMetaData {
domain: string;
name: string;
input: BlueprintInput;
input: Record<string, BlueprintInput | null>;
}
export type BlueprintInput = Record<string, any>;
export interface BlueprintInput {
name?: string;
description?: string;
selector?: Selector;
}
export interface BlueprintImportResult {
url: string;

View File

@ -94,7 +94,7 @@ export const removeEntityRegistryEntry = (
entity_id: entityId,
});
const fetchEntityRegistry = (conn) =>
export const fetchEntityRegistry = (conn) =>
conn.sendMessagePromise({
type: "config/entity_registry/list",
});

16
src/data/selector.ts Normal file
View File

@ -0,0 +1,16 @@
export type Selector = EntitySelector | DeviceSelector;
export interface EntitySelector {
entity: {
integration?: string;
domain?: string;
};
}
export interface DeviceSelector {
device: {
integration?: string;
manufacturer?: string;
model?: string;
};
}

View File

@ -26,12 +26,13 @@ import { haStyle } from "../../../resources/styles";
import { HassEntity } from "home-assistant-js-websocket";
import { navigate } from "../../../common/navigate";
import {
Blueprint,
BlueprintOrError,
Blueprints,
fetchBlueprints,
} from "../../../data/blueprint";
import "../../../components/ha-blueprint-picker";
import "../../../components/ha-circular-progress";
import "../../../components/ha-selector/ha-selector";
@customElement("blueprint-automation-editor")
export class HaBlueprintAutomationEditor extends LitElement {
@ -52,7 +53,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
this._getBlueprints();
}
private get _blueprint(): Blueprint | undefined {
private get _blueprint(): BlueprintOrError | undefined {
if (!this._blueprints) {
return undefined;
}
@ -149,9 +150,14 @@ export class HaBlueprintAutomationEditor extends LitElement {
)}
</mwc-button>
</div>
${this.config.use_blueprint.path
? blueprint?.metadata?.input &&
Object.keys(blueprint.metadata.input).length
? blueprint && "error" in blueprint
? html`<p class="warning">
There is an error in this Blueprint: ${blueprint.error}
</p>`
: blueprint?.metadata?.input &&
Object.keys(blueprint.metadata.input).length
? html`<h3>
${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.inputs"
@ -161,13 +167,23 @@ export class HaBlueprintAutomationEditor extends LitElement {
([key, value]) =>
html`<div>
${value?.description}
<paper-input
.key=${key}
.label=${value?.name || key}
.value=${this.config.use_blueprint.input &&
this.config.use_blueprint.input[key]}
@value-changed=${this._inputChanged}
></paper-input>
${value?.selector
? html`<ha-selector
.hass=${this.hass}
.selector=${value.selector}
.key=${key}
.label=${value?.name || key}
.value=${this.config.use_blueprint.input &&
this.config.use_blueprint.input[key]}
@value-changed=${this._inputChanged}
></ha-selector>`
: html`<paper-input
.key=${key}
.label=${value?.name || key}
.value=${this.config.use_blueprint.input &&
this.config.use_blueprint.input[key]}
@value-changed=${this._inputChanged}
></paper-input>`}
</div>`
)}`
: this.hass.localize(
@ -206,7 +222,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
ev.stopPropagation();
const target = ev.target as any;
const key = target.key;
const value = target.value;
const value = ev.detail.value;
if (
(this.config.use_blueprint.input &&
this.config.use_blueprint.input[key] === value) ||

View File

@ -34,6 +34,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
interface BlueprintMetaDataPath extends BlueprintMetaData {
path: string;
error: boolean;
}
const createNewFunctions = {
@ -61,10 +62,20 @@ class HaBlueprintOverview extends LitElement {
@property() public blueprints!: Blueprints;
private _processedBlueprints = memoizeOne((blueprints: Blueprints) => {
const result = Object.entries(blueprints).map(([path, blueprint]) => ({
...blueprint.metadata,
path,
}));
const result = Object.entries(blueprints).map(([path, blueprint]) => {
if ("error" in blueprint) {
return {
name: blueprint.error,
error: true,
path,
};
}
return {
...blueprint.metadata,
error: false,
path,
};
});
return result;
});
@ -98,20 +109,26 @@ class HaBlueprintOverview extends LitElement {
columns.create = {
title: "",
type: "icon-button",
template: (_, blueprint) => html`<mwc-icon-button
.blueprint=${blueprint}
@click=${(ev) => this._createNew(ev)}
><ha-svg-icon .path=${mdiPlus}></ha-svg-icon
></mwc-icon-button>`,
template: (_, blueprint: any) =>
blueprint.error
? ""
: html`<mwc-icon-button
.blueprint=${blueprint}
@click=${(ev) => this._createNew(ev)}
><ha-svg-icon .path=${mdiPlus}></ha-svg-icon
></mwc-icon-button>`,
};
columns.delete = {
title: "",
type: "icon-button",
template: (_, blueprint) => html`<mwc-icon-button
.blueprint=${blueprint}
@click=${(ev) => this._delete(ev)}
><ha-svg-icon .path=${mdiDelete}></ha-svg-icon
></mwc-icon-button>`,
template: (_, blueprint: any) =>
blueprint.error
? ""
: html`<mwc-icon-button
.blueprint=${blueprint}
@click=${(ev) => this._delete(ev)}
><ha-svg-icon .path=${mdiDelete}></ha-svg-icon
></mwc-icon-button>`,
};
return columns;
}