diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index 4b5e60f936..bc43292268 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -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` ({ - ...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)); }); diff --git a/src/components/ha-selector/ha-selector-device.ts b/src/components/ha-selector/ha-selector-device.ts new file mode 100644 index 0000000000..dbad6a4171 --- /dev/null +++ b/src/components/ha-selector/ha-selector-device.ts @@ -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` this._filterDevices(device)} + allow-custom-entity + >`; + } + + 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; + } +} diff --git a/src/components/ha-selector/ha-selector-entity.ts b/src/components/ha-selector/ha-selector-entity.ts new file mode 100644 index 0000000000..be4bfda550 --- /dev/null +++ b/src/components/ha-selector/ha-selector-entity.ts @@ -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; + + @property() public value?: any; + + @property() public label?: string; + + protected render() { + return html` this._filterEntities(entity)} + allow-custom-entity + >`; + } + + 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; + } +} diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts new file mode 100644 index 0000000000..9e80292de2 --- /dev/null +++ b/src/components/ha-selector/ha-selector.ts @@ -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; + } +} diff --git a/src/data/blueprint.ts b/src/data/blueprint.ts index b2dd307592..5381d7ff47 100644 --- a/src/data/blueprint.ts +++ b/src/data/blueprint.ts @@ -1,7 +1,9 @@ import { HomeAssistant } from "../types"; +import { Selector } from "./selector"; -export type Blueprints = Record; +export type Blueprints = Record; +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; } -export type BlueprintInput = Record; +export interface BlueprintInput { + name?: string; + description?: string; + selector?: Selector; +} export interface BlueprintImportResult { url: string; diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index 98ff504f22..179a41b496 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -94,7 +94,7 @@ export const removeEntityRegistryEntry = ( entity_id: entityId, }); -const fetchEntityRegistry = (conn) => +export const fetchEntityRegistry = (conn) => conn.sendMessagePromise({ type: "config/entity_registry/list", }); diff --git a/src/data/selector.ts b/src/data/selector.ts new file mode 100644 index 0000000000..f002adaa88 --- /dev/null +++ b/src/data/selector.ts @@ -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; + }; +} diff --git a/src/panels/config/automation/blueprint-automation-editor.ts b/src/panels/config/automation/blueprint-automation-editor.ts index 78bcf332fd..7b8103e904 100644 --- a/src/panels/config/automation/blueprint-automation-editor.ts +++ b/src/panels/config/automation/blueprint-automation-editor.ts @@ -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 { )} + ${this.config.use_blueprint.path - ? blueprint?.metadata?.input && - Object.keys(blueprint.metadata.input).length + ? blueprint && "error" in blueprint + ? html`

+ There is an error in this Blueprint: ${blueprint.error} +

` + : blueprint?.metadata?.input && + Object.keys(blueprint.metadata.input).length ? html`

${this.hass.localize( "ui.panel.config.automation.editor.blueprint.inputs" @@ -161,13 +167,23 @@ export class HaBlueprintAutomationEditor extends LitElement { ([key, value]) => html`
${value?.description} - + ${value?.selector + ? html`` + : html``}
` )}` : 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) || diff --git a/src/panels/config/blueprint/ha-blueprint-overview.ts b/src/panels/config/blueprint/ha-blueprint-overview.ts index 31c39498ad..d3874bf8f3 100644 --- a/src/panels/config/blueprint/ha-blueprint-overview.ts +++ b/src/panels/config/blueprint/ha-blueprint-overview.ts @@ -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` this._createNew(ev)} - >`, + template: (_, blueprint: any) => + blueprint.error + ? "" + : html` this._createNew(ev)} + >`, }; columns.delete = { title: "", type: "icon-button", - template: (_, blueprint) => html` this._delete(ev)} - >`, + template: (_, blueprint: any) => + blueprint.error + ? "" + : html` this._delete(ev)} + >`, }; return columns; }