mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-15 13:26:34 +00:00
Add entity and device selectors (#7735)
This commit is contained in:
parent
46f5589530
commit
f835810f0a
@ -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
|
||||
|
@ -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));
|
||||
});
|
||||
|
||||
|
81
src/components/ha-selector/ha-selector-device.ts
Normal file
81
src/components/ha-selector/ha-selector-device.ts
Normal 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;
|
||||
}
|
||||
}
|
75
src/components/ha-selector/ha-selector-entity.ts
Normal file
75
src/components/ha-selector/ha-selector-entity.ts
Normal 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;
|
||||
}
|
||||
}
|
48
src/components/ha-selector/ha-selector.ts
Normal file
48
src/components/ha-selector/ha-selector.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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
16
src/data/selector.ts
Normal 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;
|
||||
};
|
||||
}
|
@ -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) ||
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user