Add target selector (#7864)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
parent
c485ea9d7b
commit
25f7cbea5a
|
@ -0,0 +1,6 @@
|
|||
export const ensureArray = (value?: any) => {
|
||||
if (!value || Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
return [value];
|
||||
};
|
|
@ -139,7 +139,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
|||
|
||||
private _filteredDevices: DeviceRegistryEntry[] = [];
|
||||
|
||||
private _getDevices = memoizeOne(
|
||||
private _getAreasWithDevices = memoizeOne(
|
||||
(
|
||||
devices: DeviceRegistryEntry[],
|
||||
areas: AreaRegistryEntry[],
|
||||
|
@ -277,7 +277,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
|||
if (!this._devices || !this._areas || !this._entities) {
|
||||
return html``;
|
||||
}
|
||||
const areas = this._getDevices(
|
||||
const areas = this._getAreasWithDevices(
|
||||
this._devices,
|
||||
this._areas,
|
||||
this._entities,
|
||||
|
|
|
@ -111,6 +111,18 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||
@property({ type: Boolean })
|
||||
private _opened?: boolean;
|
||||
|
||||
public open() {
|
||||
this.updateComplete.then(() => {
|
||||
(this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
|
||||
});
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.updateComplete.then(() => {
|
||||
this.shadowRoot?.querySelector("paper-input")?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
private _getDevices = memoizeOne(
|
||||
(
|
||||
devices: DeviceRegistryEntry[],
|
||||
|
@ -126,14 +138,17 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||
}
|
||||
|
||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||
for (const entity of entities) {
|
||||
if (!entity.device_id) {
|
||||
continue;
|
||||
|
||||
if (includeDomains || excludeDomains || includeDeviceClasses) {
|
||||
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);
|
||||
}
|
||||
if (!(entity.device_id in deviceEntityLookup)) {
|
||||
deviceEntityLookup[entity.device_id] = [];
|
||||
}
|
||||
deviceEntityLookup[entity.device_id].push(entity);
|
||||
}
|
||||
|
||||
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
|
||||
|
|
|
@ -101,6 +101,18 @@ export class HaEntityPicker extends LitElement {
|
|||
|
||||
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
|
||||
|
||||
public open() {
|
||||
this.updateComplete.then(() => {
|
||||
(this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
|
||||
});
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.updateComplete.then(() => {
|
||||
this.shadowRoot?.querySelector("paper-input")?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
private _initedStates = false;
|
||||
|
||||
private _states: HassEntity[] = [];
|
||||
|
|
|
@ -29,6 +29,17 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
|||
import { PolymerChangedEvent } from "../polymer-types";
|
||||
import { HomeAssistant } from "../types";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
DeviceEntityLookup,
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../data/entity_registry";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
|
||||
const rowRenderer = (
|
||||
root: HTMLElement,
|
||||
|
@ -71,39 +82,225 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
|||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property() public _areas?: AreaRegistryEntry[];
|
||||
|
||||
@property({ type: Boolean, attribute: "no-add" })
|
||||
public noAdd?: boolean;
|
||||
|
||||
/**
|
||||
* Show only areas with entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show no areas with entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only areas with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
|
||||
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
|
||||
|
||||
@internalProperty() private _areas?: AreaRegistryEntry[];
|
||||
|
||||
@internalProperty() private _devices?: DeviceRegistryEntry[];
|
||||
|
||||
@internalProperty() private _entities?: EntityRegistryEntry[];
|
||||
|
||||
@internalProperty() private _opened?: boolean;
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeAreaRegistry(this.hass.connection!, (areas) => {
|
||||
this._areas = this.noAdd
|
||||
? areas
|
||||
: [
|
||||
...areas,
|
||||
{
|
||||
area_id: "add_new",
|
||||
name: this.hass.localize("ui.components.area-picker.add_new"),
|
||||
},
|
||||
];
|
||||
this._areas = areas;
|
||||
}),
|
||||
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||
this._devices = devices;
|
||||
}),
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
this._entities = entities;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public open() {
|
||||
this.updateComplete.then(() => {
|
||||
(this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
|
||||
});
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.updateComplete.then(() => {
|
||||
this.shadowRoot?.querySelector("paper-input")?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
private _getAreas = memoizeOne(
|
||||
(
|
||||
areas: AreaRegistryEntry[],
|
||||
devices: DeviceRegistryEntry[],
|
||||
entities: EntityRegistryEntry[],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
deviceFilter: this["deviceFilter"],
|
||||
entityFilter: this["entityFilter"],
|
||||
noAdd: this["noAdd"]
|
||||
): AreaRegistryEntry[] => {
|
||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
let inputEntities: EntityRegistryEntry[] | undefined;
|
||||
|
||||
if (includeDomains || excludeDomains || includeDeviceClasses) {
|
||||
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);
|
||||
}
|
||||
inputDevices = devices;
|
||||
inputEntities = entities.filter((entity) => entity.area_id);
|
||||
} else if (deviceFilter) {
|
||||
inputDevices = devices;
|
||||
} else if (entityFilter) {
|
||||
inputEntities = entities.filter((entity) => entity.area_id);
|
||||
}
|
||||
|
||||
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))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter((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))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter(
|
||||
(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)
|
||||
);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (deviceFilter) {
|
||||
inputDevices = inputDevices!.filter((device) => deviceFilter!(device));
|
||||
}
|
||||
|
||||
if (entityFilter) {
|
||||
entities = entities.filter((entity) => entityFilter!(entity));
|
||||
}
|
||||
|
||||
let outputAreas = areas;
|
||||
|
||||
let areaIds: string[] | undefined;
|
||||
|
||||
if (inputDevices) {
|
||||
areaIds = inputDevices
|
||||
.filter((device) => device.area_id)
|
||||
.map((device) => device.area_id!);
|
||||
}
|
||||
|
||||
if (inputEntities) {
|
||||
areaIds = (areaIds ?? []).concat(
|
||||
inputEntities
|
||||
.filter((entity) => entity.area_id)
|
||||
.map((entity) => entity.area_id!)
|
||||
);
|
||||
}
|
||||
|
||||
if (areaIds) {
|
||||
outputAreas = areas.filter((area) => areaIds!.includes(area.area_id));
|
||||
}
|
||||
|
||||
return noAdd
|
||||
? outputAreas
|
||||
: [
|
||||
...outputAreas,
|
||||
{
|
||||
area_id: "add_new",
|
||||
name: this.hass.localize("ui.components.area-picker.add_new"),
|
||||
},
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._areas) {
|
||||
if (!this._devices || !this._areas || !this._entities) {
|
||||
return html``;
|
||||
}
|
||||
const areas = this._getAreas(
|
||||
this._areas,
|
||||
this._devices,
|
||||
this._entities,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.deviceFilter,
|
||||
this.entityFilter,
|
||||
this.noAdd
|
||||
);
|
||||
return html`
|
||||
<vaadin-combo-box-light
|
||||
item-value-path="area_id"
|
||||
item-id-path="area_id"
|
||||
item-label-path="name"
|
||||
.items=${this._areas}
|
||||
.items=${areas}
|
||||
.value=${this._value}
|
||||
.renderer=${rowRenderer}
|
||||
@opened-changed=${this._openedChanged}
|
||||
|
@ -138,7 +335,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
|||
</ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
${this._areas.length > 0
|
||||
${areas.length > 0
|
||||
? html`
|
||||
<ha-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { ToggleButton } from "../types";
|
||||
import "./ha-svg-icon";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
|
||||
@customElement("ha-button-toggle-group")
|
||||
export class HaButtonToggleGroup extends LitElement {
|
||||
|
@ -21,17 +22,22 @@ export class HaButtonToggleGroup extends LitElement {
|
|||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div>
|
||||
${this.buttons.map(
|
||||
(button) => html`
|
||||
<mwc-icon-button
|
||||
.label=${button.label}
|
||||
.value=${button.value}
|
||||
?active=${this.active === button.value}
|
||||
@click=${this._handleClick}
|
||||
>
|
||||
<ha-svg-icon .path=${button.iconPath}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
${this.buttons.map((button) =>
|
||||
button.iconPath
|
||||
? html`<mwc-icon-button
|
||||
.label=${button.label}
|
||||
.value=${button.value}
|
||||
?active=${this.active === button.value}
|
||||
@click=${this._handleClick}
|
||||
>
|
||||
<ha-svg-icon .path=${button.iconPath}></ha-svg-icon>
|
||||
</mwc-icon-button>`
|
||||
: html`<mwc-button
|
||||
.value=${button.value}
|
||||
?active=${this.active === button.value}
|
||||
@click=${this._handleClick}
|
||||
>${button.label}</mwc-button
|
||||
>`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
|
@ -49,13 +55,15 @@ export class HaButtonToggleGroup extends LitElement {
|
|||
--mdc-icon-button-size: var(--button-toggle-size, 36px);
|
||||
--mdc-icon-size: var(--button-toggle-icon-size, 20px);
|
||||
}
|
||||
mwc-icon-button {
|
||||
mwc-icon-button,
|
||||
mwc-button {
|
||||
border: 1px solid var(--primary-color);
|
||||
border-right-width: 0px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
mwc-icon-button::before {
|
||||
mwc-icon-button::before,
|
||||
mwc-button::before {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
|
@ -67,17 +75,21 @@ export class HaButtonToggleGroup extends LitElement {
|
|||
content: "";
|
||||
transition: opacity 15ms linear, background-color 15ms linear;
|
||||
}
|
||||
mwc-icon-button[active]::before {
|
||||
mwc-icon-button[active]::before,
|
||||
mwc-button[active]::before {
|
||||
opacity: var(--mdc-icon-button-ripple-opacity, 0.12);
|
||||
}
|
||||
mwc-icon-button:first-child {
|
||||
mwc-icon-button:first-child,
|
||||
mwc-button:first-child {
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
mwc-icon-button:last-child {
|
||||
mwc-icon-button:last-child,
|
||||
mwc-button:last-child {
|
||||
border-radius: 0 4px 4px 0;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
mwc-icon-button:only-child {
|
||||
mwc-icon-button:only-child,
|
||||
mwc-button:only-child {
|
||||
border-radius: 4px;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { ActionSelector } from "../../data/selector";
|
||||
import { Action } from "../../data/script";
|
||||
import "../../panels/config/automation/action/ha-automation-action";
|
||||
|
||||
@customElement("ha-selector-action")
|
||||
export class HaActionSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: ActionSelector;
|
||||
|
||||
@property() public value?: Action;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
protected render() {
|
||||
return html`<ha-automation-action
|
||||
.actions=${this.value || []}
|
||||
.hass=${this.hass}
|
||||
></ha-automation-action>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-automation-action {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-action": HaActionSelector;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,16 @@
|
|||
import { customElement, html, LitElement, property } from "lit-element";
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { AreaSelector } from "../../data/selector";
|
||||
import "../ha-area-picker";
|
||||
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
|
||||
import { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
|
||||
@customElement("ha-selector-area")
|
||||
export class HaAreaSelector extends LitElement {
|
||||
|
@ -13,14 +22,76 @@ export class HaAreaSelector extends LitElement {
|
|||
|
||||
@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.area.device?.integration
|
||||
) {
|
||||
this._loadConfigEntries();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
no-add
|
||||
.deviceFilter=${(device) => this._filterDevices(device)}
|
||||
.entityFilter=${(entity) => this._filterEntities(entity)}
|
||||
.includeDeviceClasses=${this.selector.area.entity?.device_class
|
||||
? [this.selector.area.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.area.entity?.domain
|
||||
? [this.selector.area.entity.domain]
|
||||
: undefined}
|
||||
></ha-area-picker>`;
|
||||
}
|
||||
|
||||
private _filterEntities(entity: EntityRegistryEntry): boolean {
|
||||
if (this.selector.area.entity?.integration) {
|
||||
if (entity.platform !== this.selector.area.entity.integration) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _filterDevices(device: DeviceRegistryEntry): boolean {
|
||||
if (
|
||||
this.selector.area.device?.manufacturer &&
|
||||
device.manufacturer !== this.selector.area.device.manufacturer
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
this.selector.area.device?.model &&
|
||||
device.model !== this.selector.area.device.model
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (this.selector.area.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.area.device?.integration
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
|
@ -19,7 +19,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
|||
|
||||
@property() public selector!: EntitySelector;
|
||||
|
||||
@internalProperty() private _entities?: Record<string, string>;
|
||||
@internalProperty() private _entityPlaformLookup?: Record<string, string>;
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
|
@ -45,7 +45,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
|||
}
|
||||
entityLookup[confEnt.entity_id] = confEnt.platform;
|
||||
}
|
||||
this._entities = entityLookup;
|
||||
this._entityPlaformLookup = entityLookup;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
@ -66,8 +66,9 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
|||
}
|
||||
if (this.selector.entity.integration) {
|
||||
if (
|
||||
!this._entities ||
|
||||
this._entities[entity.entity_id] !== this.selector.entity.integration
|
||||
!this._entityPlaformLookup ||
|
||||
this._entityPlaformLookup[entity.entity_id] !==
|
||||
this.selector.entity.integration
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { TargetSelector } from "../../data/selector";
|
||||
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
|
||||
import { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import "../ha-target-picker";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../data/entity_registry";
|
||||
import { Target } from "../../data/target";
|
||||
import "@material/mwc-tab-bar/mwc-tab-bar";
|
||||
import "@material/mwc-tab/mwc-tab";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
|
||||
@customElement("ha-selector-target")
|
||||
export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: TargetSelector;
|
||||
|
||||
@property() public value?: Target;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@internalProperty() private _entityPlaformLookup?: Record<string, string>;
|
||||
|
||||
@internalProperty() private _configEntries?: ConfigEntry[];
|
||||
|
||||
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._entityPlaformLookup = entityLookup;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProperties) {
|
||||
if (changedProperties.has("selector")) {
|
||||
const oldSelector = changedProperties.get("selector");
|
||||
if (
|
||||
oldSelector !== this.selector &&
|
||||
this.selector.target.device?.integration
|
||||
) {
|
||||
this._loadConfigEntries();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`<ha-target-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.deviceFilter=${(device) => this._filterDevices(device)}
|
||||
.entityRegFilter=${(entity: EntityRegistryEntry) =>
|
||||
this._filterRegEntities(entity)}
|
||||
.entityFilter=${(entity: HassEntity) => this._filterEntities(entity)}
|
||||
.includeDeviceClasses=${this.selector.target.entity?.device_class
|
||||
? [this.selector.target.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.target.entity?.domain
|
||||
? [this.selector.target.entity.domain]
|
||||
: undefined}
|
||||
></ha-target-picker>`;
|
||||
}
|
||||
|
||||
private _filterEntities(entity: HassEntity): boolean {
|
||||
if (this.selector.target.entity?.integration) {
|
||||
if (
|
||||
!this._entityPlaformLookup ||
|
||||
this._entityPlaformLookup[entity.entity_id] !==
|
||||
this.selector.target.entity.integration
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _filterRegEntities(entity: EntityRegistryEntry): boolean {
|
||||
if (this.selector.target.entity?.integration) {
|
||||
if (entity.platform !== this.selector.target.entity.integration) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _filterDevices(device: DeviceRegistryEntry): boolean {
|
||||
if (
|
||||
this.selector.target.device?.manufacturer &&
|
||||
device.manufacturer !== this.selector.target.device.manufacturer
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
this.selector.target.device?.model &&
|
||||
device.model !== this.selector.target.device.model
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (this.selector.target.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.target.device?.integration
|
||||
);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-target-picker {
|
||||
margin: 0 -8px;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-target": HaTargetSelector;
|
||||
}
|
||||
}
|
|
@ -5,9 +5,11 @@ import { HomeAssistant } from "../../types";
|
|||
import "./ha-selector-entity";
|
||||
import "./ha-selector-device";
|
||||
import "./ha-selector-area";
|
||||
import "./ha-selector-target";
|
||||
import "./ha-selector-number";
|
||||
import "./ha-selector-boolean";
|
||||
import "./ha-selector-time";
|
||||
import "./ha-selector-action";
|
||||
import { Selector } from "../../data/selector";
|
||||
|
||||
@customElement("ha-selector")
|
||||
|
|
|
@ -0,0 +1,595 @@
|
|||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
query,
|
||||
unsafeCSS,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../types";
|
||||
// @ts-ignore
|
||||
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
|
||||
import {
|
||||
mdiSofa,
|
||||
mdiDevices,
|
||||
mdiClose,
|
||||
mdiPlus,
|
||||
mdiUnfoldMoreVertical,
|
||||
} from "@mdi/js";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-icon";
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../data/area_registry";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../data/entity_registry";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { stateIcon } from "../common/entity/state_icon";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { Target } from "../data/target";
|
||||
import { ensureArray } from "../common/ensure-array";
|
||||
import "./entity/ha-entity-picker";
|
||||
import "./device/ha-device-picker";
|
||||
import "./ha-area-picker";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./entity/ha-entity-picker";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
|
||||
@customElement("ha-target-picker")
|
||||
export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public value?: Target;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
/**
|
||||
* Show only targets with entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only targets with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
|
||||
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property() public entityRegFilter?: (entity: EntityRegistryEntry) => boolean;
|
||||
|
||||
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
|
||||
@internalProperty() private _areas?: { [areaId: string]: AreaRegistryEntry };
|
||||
|
||||
@internalProperty() private _devices?: {
|
||||
[deviceId: string]: DeviceRegistryEntry;
|
||||
};
|
||||
|
||||
@internalProperty() private _entities?: EntityRegistryEntry[];
|
||||
|
||||
@internalProperty() private _addMode?: "area_id" | "entity_id" | "device_id";
|
||||
|
||||
@query("#input") private _inputElement?;
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeAreaRegistry(this.hass.connection!, (areas) => {
|
||||
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
|
||||
for (const area of areas) {
|
||||
areaLookup[area.area_id] = area;
|
||||
}
|
||||
this._areas = areaLookup;
|
||||
}),
|
||||
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||
const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {};
|
||||
for (const device of devices) {
|
||||
deviceLookup[device.id] = device;
|
||||
}
|
||||
this._devices = deviceLookup;
|
||||
}),
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
this._entities = entities;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._areas || !this._devices || !this._entities) {
|
||||
return html``;
|
||||
}
|
||||
return html`<div class="mdc-chip-set items">
|
||||
${ensureArray(this.value?.area_id)?.map((area_id) => {
|
||||
const area = this._areas![area_id];
|
||||
return this._renderChip(
|
||||
"area_id",
|
||||
area_id,
|
||||
area?.name || area_id,
|
||||
undefined,
|
||||
mdiSofa
|
||||
);
|
||||
})}
|
||||
${ensureArray(this.value?.device_id)?.map((device_id) => {
|
||||
const device = this._devices![device_id];
|
||||
return this._renderChip(
|
||||
"device_id",
|
||||
device_id,
|
||||
device?.name || device_id,
|
||||
undefined,
|
||||
mdiDevices
|
||||
);
|
||||
})}
|
||||
${ensureArray(this.value?.entity_id)?.map((entity_id) => {
|
||||
const entity = this.hass.states[entity_id];
|
||||
return this._renderChip(
|
||||
"entity_id",
|
||||
entity_id,
|
||||
entity ? computeStateName(entity) : entity_id,
|
||||
entity ? stateIcon(entity) : undefined
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
${this._renderPicker()}
|
||||
<div class="mdc-chip-set">
|
||||
<div
|
||||
class="mdc-chip area_id add"
|
||||
.type=${"area_id"}
|
||||
@click=${this._showPicker}
|
||||
>
|
||||
<div class="mdc-chip__ripple"></div>
|
||||
<ha-svg-icon
|
||||
class="mdc-chip__icon mdc-chip__icon--leading"
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
<span role="gridcell">
|
||||
<span role="button" tabindex="0" class="mdc-chip__primary-action">
|
||||
<span class="mdc-chip__text"
|
||||
>${this.hass.localize(
|
||||
"ui.components.target-picker.add_area_id"
|
||||
)}</span
|
||||
>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mdc-chip device_id add"
|
||||
.type=${"device_id"}
|
||||
@click=${this._showPicker}
|
||||
>
|
||||
<div class="mdc-chip__ripple"></div>
|
||||
<ha-svg-icon
|
||||
class="mdc-chip__icon mdc-chip__icon--leading"
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
<span role="gridcell">
|
||||
<span role="button" tabindex="0" class="mdc-chip__primary-action">
|
||||
<span class="mdc-chip__text"
|
||||
>${this.hass.localize(
|
||||
"ui.components.target-picker.add_device_id"
|
||||
)}</span
|
||||
>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mdc-chip entity_id add"
|
||||
.type=${"entity_id"}
|
||||
@click=${this._showPicker}
|
||||
>
|
||||
<div class="mdc-chip__ripple"></div>
|
||||
<ha-svg-icon
|
||||
class="mdc-chip__icon mdc-chip__icon--leading"
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
<span role="gridcell">
|
||||
<span role="button" tabindex="0" class="mdc-chip__primary-action">
|
||||
<span class="mdc-chip__text"
|
||||
>${this.hass.localize(
|
||||
"ui.components.target-picker.add_entity_id"
|
||||
)}</span
|
||||
>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private async _showPicker(ev) {
|
||||
this._addMode = ev.currentTarget.type;
|
||||
await this.updateComplete;
|
||||
setTimeout(() => {
|
||||
this._inputElement?.open();
|
||||
this._inputElement?.focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private _renderChip(
|
||||
type: string,
|
||||
id: string,
|
||||
name: string,
|
||||
icon?: string,
|
||||
iconPath?: string
|
||||
) {
|
||||
return html`
|
||||
<div
|
||||
class="mdc-chip ${classMap({
|
||||
[type]: true,
|
||||
})}"
|
||||
>
|
||||
${iconPath
|
||||
? html`<ha-svg-icon
|
||||
class="mdc-chip__icon mdc-chip__icon--leading"
|
||||
.path=${iconPath}
|
||||
></ha-svg-icon>`
|
||||
: ""}
|
||||
${icon
|
||||
? html`<ha-icon
|
||||
class="mdc-chip__icon mdc-chip__icon--leading"
|
||||
.icon=${icon}
|
||||
></ha-icon>`
|
||||
: ""}
|
||||
<span role="gridcell">
|
||||
<span role="button" tabindex="0" class="mdc-chip__primary-action">
|
||||
<span class="mdc-chip__text">${name}</span>
|
||||
</span>
|
||||
</span>
|
||||
${type === "entity_id"
|
||||
? ""
|
||||
: html` <span role="gridcell">
|
||||
<mwc-icon-button
|
||||
class="expand-btn mdc-chip__icon mdc-chip__icon--trailing"
|
||||
tabindex="-1"
|
||||
role="button"
|
||||
.label=${"Expand"}
|
||||
.id=${id}
|
||||
.type=${type}
|
||||
@click=${this._handleExpand}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiUnfoldMoreVertical}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<paper-tooltip animation-delay="0"
|
||||
>${this.hass.localize(
|
||||
`ui.components.target-picker.expand_${type}`
|
||||
)}</paper-tooltip
|
||||
>
|
||||
</span>`}
|
||||
<span role="gridcell">
|
||||
<mwc-icon-button
|
||||
class="mdc-chip__icon mdc-chip__icon--trailing"
|
||||
tabindex="-1"
|
||||
role="button"
|
||||
.label=${"Remove"}
|
||||
.id=${id}
|
||||
.type=${type}
|
||||
@click=${this._handleRemove}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<paper-tooltip animation-delay="0"
|
||||
>${this.hass.localize(
|
||||
`ui.components.target-picker.remove_${type}`
|
||||
)}</paper-tooltip
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPicker() {
|
||||
switch (this._addMode) {
|
||||
case "area_id":
|
||||
return html`<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"area_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_area_id"
|
||||
)}
|
||||
no-add
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityRegFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@value-changed=${this._targetPicked}
|
||||
></ha-area-picker>`;
|
||||
case "device_id":
|
||||
return html`<ha-device-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"device_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_device_id"
|
||||
)}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityRegFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@value-changed=${this._targetPicked}
|
||||
></ha-device-picker>`;
|
||||
case "entity_id":
|
||||
return html`<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"entity_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_entity_id"
|
||||
)}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@value-changed=${this._targetPicked}
|
||||
></ha-entity-picker>`;
|
||||
}
|
||||
return html``;
|
||||
}
|
||||
|
||||
private _targetPicked(ev) {
|
||||
ev.stopPropagation();
|
||||
if (!ev.detail.value) {
|
||||
return;
|
||||
}
|
||||
const value = ev.detail.value;
|
||||
const target = ev.currentTarget;
|
||||
target.value = "";
|
||||
this._addMode = undefined;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: this.value
|
||||
? {
|
||||
...this.value,
|
||||
[target.type]: this.value[target.type]
|
||||
? [...ensureArray(this.value[target.type]), value]
|
||||
: value,
|
||||
}
|
||||
: { [target.type]: value },
|
||||
});
|
||||
}
|
||||
|
||||
private _handleExpand(ev) {
|
||||
const target = ev.currentTarget as any;
|
||||
const newDevices: string[] = [];
|
||||
const newEntities: string[] = [];
|
||||
if (target.type === "area_id") {
|
||||
Object.values(this._devices!).forEach((device) => {
|
||||
if (
|
||||
device.area_id === target.id &&
|
||||
!this.value!.device_id?.includes(device.id) &&
|
||||
this._deviceMeetsFilter(device)
|
||||
) {
|
||||
newDevices.push(device.id);
|
||||
}
|
||||
});
|
||||
this._entities!.forEach((entity) => {
|
||||
if (
|
||||
entity.area_id === target.id &&
|
||||
!this.value!.entity_id?.includes(entity.entity_id) &&
|
||||
this._entityRegMeetsFilter(entity)
|
||||
) {
|
||||
newEntities.push(entity.entity_id);
|
||||
}
|
||||
});
|
||||
} else if (target.type === "device_id") {
|
||||
this._entities!.forEach((entity) => {
|
||||
if (
|
||||
entity.device_id === target.id &&
|
||||
!this.value!.entity_id?.includes(entity.entity_id) &&
|
||||
this._entityRegMeetsFilter(entity)
|
||||
) {
|
||||
newEntities.push(entity.entity_id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
let value = this.value;
|
||||
if (newEntities.length) {
|
||||
value = this._addItems(value, "entity_id", newEntities);
|
||||
}
|
||||
if (newDevices.length) {
|
||||
value = this._addItems(value, "device_id", newDevices);
|
||||
}
|
||||
value = this._removeItem(value, target.type, target.id);
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
private _handleRemove(ev) {
|
||||
const target = ev.currentTarget as any;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: this._removeItem(this.value, target.type, target.id),
|
||||
});
|
||||
}
|
||||
|
||||
private _addItems(
|
||||
value: this["value"],
|
||||
type: string,
|
||||
ids: string[]
|
||||
): this["value"] {
|
||||
return {
|
||||
...value,
|
||||
[type]: value![type] ? ensureArray(value![type])!.concat(ids) : ids,
|
||||
};
|
||||
}
|
||||
|
||||
private _removeItem(
|
||||
value: this["value"],
|
||||
type: string,
|
||||
id: string
|
||||
): this["value"] {
|
||||
return {
|
||||
...value,
|
||||
[type]: ensureArray(value![type])!.filter((val) => val !== id),
|
||||
};
|
||||
}
|
||||
|
||||
private _deviceMeetsFilter(device: DeviceRegistryEntry): boolean {
|
||||
const devEntities = this._entities?.filter(
|
||||
(entity) => entity.device_id === device.id
|
||||
);
|
||||
if (this.includeDomains) {
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!devEntities.some((entity) =>
|
||||
this.includeDomains!.includes(computeDomain(entity.entity_id))
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.includeDeviceClasses) {
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!devEntities.some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
this.includeDeviceClasses!.includes(
|
||||
stateObj.attributes.device_class
|
||||
)
|
||||
);
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.deviceFilter) {
|
||||
return this.deviceFilter(device);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _entityRegMeetsFilter(entity: EntityRegistryEntry): boolean {
|
||||
if (
|
||||
this.includeDomains &&
|
||||
!this.includeDomains.includes(computeDomain(entity.entity_id))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (this.includeDeviceClasses) {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!stateObj.attributes.device_class ||
|
||||
!this.includeDeviceClasses!.includes(stateObj.attributes.device_class)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.entityRegFilter) {
|
||||
return this.entityRegFilter(entity);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
${unsafeCSS(chipStyles)}
|
||||
.mdc-chip {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.items {
|
||||
z-index: 2;
|
||||
}
|
||||
.mdc-chip.add {
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
.mdc-chip:not(.add) {
|
||||
cursor: default;
|
||||
}
|
||||
.mdc-chip mwc-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
outline: none;
|
||||
}
|
||||
.mdc-chip mwc-icon-button ha-svg-icon {
|
||||
border-radius: 50%;
|
||||
background: var(--secondary-text-color);
|
||||
}
|
||||
.mdc-chip__icon.mdc-chip__icon--trailing {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
--mdc-icon-size: 14px;
|
||||
color: var(--card-background-color);
|
||||
}
|
||||
.mdc-chip__icon--leading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
--mdc-icon-size: 20px;
|
||||
border-radius: 50%;
|
||||
padding: 6px;
|
||||
margin-left: -14px !important;
|
||||
}
|
||||
.expand-btn {
|
||||
margin-right: 0;
|
||||
}
|
||||
.mdc-chip.area_id:not(.add) {
|
||||
border: 2px solid #fed6a4;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
.mdc-chip.area_id:not(.add) .mdc-chip__icon--leading,
|
||||
.mdc-chip.area_id.add {
|
||||
background: #fed6a4;
|
||||
}
|
||||
.mdc-chip.device_id:not(.add) {
|
||||
border: 2px solid #a8e1fb;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
.mdc-chip.device_id:not(.add) .mdc-chip__icon--leading,
|
||||
.mdc-chip.device_id.add {
|
||||
background: #a8e1fb;
|
||||
}
|
||||
.mdc-chip.entity_id:not(.add) {
|
||||
border: 2px solid #d2e7b9;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
.mdc-chip.entity_id:not(.add) .mdc-chip__icon--leading,
|
||||
.mdc-chip.entity_id.add {
|
||||
background: #d2e7b9;
|
||||
}
|
||||
.mdc-chip:hover {
|
||||
z-index: 5;
|
||||
}
|
||||
paper-tooltip {
|
||||
min-width: 200px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-target-picker": HaTargetPicker;
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ import { navigate } from "../common/navigate";
|
|||
import { Context, HomeAssistant } from "../types";
|
||||
import { BlueprintInput } from "./blueprint";
|
||||
import { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
import { Action } from "./script";
|
||||
import { Action, MODES } from "./script";
|
||||
|
||||
export interface AutomationEntity extends HassEntityBase {
|
||||
attributes: HassEntityAttributeBase & {
|
||||
|
@ -26,7 +26,7 @@ export interface ManualAutomationConfig {
|
|||
trigger: Trigger[];
|
||||
condition?: Condition[];
|
||||
action: Action[];
|
||||
mode?: "single" | "restart" | "queued" | "parallel";
|
||||
mode?: typeof MODES[number];
|
||||
max?: number;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,13 +7,13 @@ import { navigate } from "../common/navigate";
|
|||
import { HomeAssistant } from "../types";
|
||||
import { Condition, Trigger } from "./automation";
|
||||
|
||||
export const MODES = ["single", "restart", "queued", "parallel"];
|
||||
export const MODES = ["single", "restart", "queued", "parallel"] as const;
|
||||
export const MODES_MAX = ["queued", "parallel"];
|
||||
|
||||
export interface ScriptEntity extends HassEntityBase {
|
||||
attributes: HassEntityAttributeBase & {
|
||||
last_triggered: string;
|
||||
mode: "single" | "restart" | "queued" | "parallel";
|
||||
mode: typeof MODES[number];
|
||||
current?: number;
|
||||
max?: number;
|
||||
};
|
||||
|
@ -23,7 +23,7 @@ export interface ScriptConfig {
|
|||
alias: string;
|
||||
sequence: Action[];
|
||||
icon?: string;
|
||||
mode?: "single" | "restart" | "queued" | "parallel";
|
||||
mode?: typeof MODES[number];
|
||||
max?: number;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,11 @@ export type Selector =
|
|||
| EntitySelector
|
||||
| DeviceSelector
|
||||
| AreaSelector
|
||||
| TargetSelector
|
||||
| NumberSelector
|
||||
| BooleanSelector
|
||||
| TimeSelector;
|
||||
| TimeSelector
|
||||
| ActionSelector;
|
||||
|
||||
export interface EntitySelector {
|
||||
entity: {
|
||||
|
@ -19,13 +21,41 @@ export interface DeviceSelector {
|
|||
integration?: string;
|
||||
manufacturer?: string;
|
||||
model?: string;
|
||||
entity?: EntitySelector["entity"];
|
||||
entity?: {
|
||||
domain?: EntitySelector["entity"]["domain"];
|
||||
device_class?: EntitySelector["entity"]["device_class"];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface AreaSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
area: {};
|
||||
area: {
|
||||
entity?: {
|
||||
integration?: EntitySelector["entity"]["integration"];
|
||||
domain?: EntitySelector["entity"]["domain"];
|
||||
device_class?: EntitySelector["entity"]["device_class"];
|
||||
};
|
||||
device?: {
|
||||
integration?: DeviceSelector["device"]["integration"];
|
||||
manufacturer?: DeviceSelector["device"]["manufacturer"];
|
||||
model?: DeviceSelector["device"]["model"];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface TargetSelector {
|
||||
target: {
|
||||
entity?: {
|
||||
integration?: EntitySelector["entity"]["integration"];
|
||||
domain?: EntitySelector["entity"]["domain"];
|
||||
device_class?: EntitySelector["entity"]["device_class"];
|
||||
};
|
||||
device?: {
|
||||
integration?: DeviceSelector["device"]["integration"];
|
||||
manufacturer?: DeviceSelector["device"]["manufacturer"];
|
||||
model?: DeviceSelector["device"]["model"];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface NumberSelector {
|
||||
|
@ -47,3 +77,8 @@ export interface TimeSelector {
|
|||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
time: {};
|
||||
}
|
||||
|
||||
export interface ActionSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
action: {};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export interface Target {
|
||||
entity_id?: string[];
|
||||
device_id?: string[];
|
||||
area_id?: string[];
|
||||
}
|
|
@ -63,7 +63,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
|||
|
||||
protected render() {
|
||||
const blueprint = this._blueprint;
|
||||
return html`<ha-config-section .isWide=${this.isWide}>
|
||||
return html`<ha-config-section vertical .isWide=${this.isWide}>
|
||||
${!this.narrow
|
||||
? html` <span slot="header">${this.config.alias}</span> `
|
||||
: ""}
|
||||
|
@ -119,7 +119,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
|||
</ha-card>
|
||||
</ha-config-section>
|
||||
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<ha-config-section vertical .isWide=${this.isWide}>
|
||||
<span slot="header"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.blueprint.header"
|
||||
|
@ -185,6 +185,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
|||
></ha-selector>`
|
||||
: html`<paper-input
|
||||
.key=${key}
|
||||
required
|
||||
.value=${this.config.use_blueprint.input &&
|
||||
this.config.use_blueprint.input[key]}
|
||||
@value-changed=${this._inputChanged}
|
||||
|
@ -275,9 +276,6 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
|||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
.padding {
|
||||
padding: 16px;
|
||||
}
|
||||
|
@ -304,10 +302,10 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
|||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
:host(:not([narrow])) ha-settings-row paper-input {
|
||||
width: 50%;
|
||||
width: 60%;
|
||||
}
|
||||
:host(:not([narrow])) ha-settings-row ha-selector {
|
||||
width: 50%;
|
||||
width: 60%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
|
|
@ -206,6 +206,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
|||
? html`<blueprint-automation-editor
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
.stateObj=${stateObj}
|
||||
.config=${this._config}
|
||||
@value-changed=${this._valueChanged}
|
||||
|
@ -213,6 +214,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
|||
: html`<manual-automation-editor
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
.stateObj=${stateObj}
|
||||
.config=${this._config}
|
||||
@value-changed=${this._valueChanged}
|
||||
|
|
|
@ -5,6 +5,8 @@ import { classMap } from "lit-html/directives/class-map";
|
|||
export class HaConfigSection extends LitElement {
|
||||
@property() public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public vertical = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div
|
||||
|
@ -16,8 +18,8 @@ export class HaConfigSection extends LitElement {
|
|||
<div
|
||||
class="together layout ${classMap({
|
||||
narrow: !this.isWide,
|
||||
vertical: !this.isWide,
|
||||
horizontal: this.isWide,
|
||||
vertical: this.vertical || !this.isWide,
|
||||
horizontal: !this.vertical && this.isWide,
|
||||
})}"
|
||||
>
|
||||
<div class="intro"><slot name="introduction"></slot></div>
|
||||
|
|
|
@ -334,6 +334,16 @@
|
|||
"show_attributes": "Show attributes"
|
||||
}
|
||||
},
|
||||
"target-picker": {
|
||||
"expand_area_id": "Expand this area in the seperate devices and entities that it contains. After expanding it will not update the devices and entities when the area changes.",
|
||||
"expand_device_id": "Expand this device in seperate entities. After expanding it will not update the entities when the device changes.",
|
||||
"remove_area_id": "Remove area",
|
||||
"remove_device_id": "Remove device",
|
||||
"remove_entity_id": "Remove entity",
|
||||
"add_area_id": "Pick area",
|
||||
"add_device_id": "Pick device",
|
||||
"add_entity_id": "Pick entity"
|
||||
},
|
||||
"user-picker": {
|
||||
"no_user": "No user",
|
||||
"add_user": "Add user",
|
||||
|
|
Loading…
Reference in New Issue