Complete filtering of selectors

This commit is contained in:
Bram Kragten 2020-12-01 11:13:08 +01:00
parent 8d7ba19a08
commit 4c2ca9224d
6 changed files with 328 additions and 44 deletions

View File

@ -139,7 +139,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
private _filteredDevices: DeviceRegistryEntry[] = []; private _filteredDevices: DeviceRegistryEntry[] = [];
private _getDevices = memoizeOne( private _getAreasWithDevices = memoizeOne(
( (
devices: DeviceRegistryEntry[], devices: DeviceRegistryEntry[],
areas: AreaRegistryEntry[], areas: AreaRegistryEntry[],
@ -277,7 +277,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
if (!this._devices || !this._areas || !this._entities) { if (!this._devices || !this._areas || !this._entities) {
return html``; return html``;
} }
const areas = this._getDevices( const areas = this._getAreasWithDevices(
this._devices, this._devices,
this._areas, this._areas,
this._entities, this._entities,

View File

@ -126,14 +126,17 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
} }
const deviceEntityLookup: DeviceEntityLookup = {}; const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) {
if (!entity.device_id) { if (includeDomains || excludeDomains || includeDeviceClasses) {
continue; 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 } = {}; const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};

View File

@ -29,6 +29,17 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../polymer-types"; import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import memoizeOne from "memoize-one"; 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 = ( const rowRenderer = (
root: HTMLElement, root: HTMLElement,
@ -71,39 +82,213 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
@property() public placeholder?: string; @property() public placeholder?: string;
@property() public _areas?: AreaRegistryEntry[];
@property({ type: Boolean, attribute: "no-add" }) @property({ type: Boolean, attribute: "no-add" })
public noAdd?: boolean; 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; @internalProperty() private _opened?: boolean;
public hassSubscribe(): UnsubscribeFunc[] { public hassSubscribe(): UnsubscribeFunc[] {
return [ return [
subscribeAreaRegistry(this.hass.connection!, (areas) => { subscribeAreaRegistry(this.hass.connection!, (areas) => {
this._areas = this.noAdd this._areas = areas;
? areas }),
: [ subscribeDeviceRegistry(this.hass.connection!, (devices) => {
...areas, this._devices = devices;
{ }),
area_id: "add_new", subscribeEntityRegistry(this.hass.connection!, (entities) => {
name: this.hass.localize("ui.components.area-picker.add_new"), this._entities = entities;
},
];
}), }),
]; ];
} }
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.device_id);
} else if (deviceFilter) {
inputDevices = [...devices];
} else if (entityFilter) {
inputEntities = entities.filter((entity) => !entity.device_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 { protected render(): TemplateResult {
if (!this._areas) { if (!this._devices || !this._areas || !this._entities) {
return html``; 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` return html`
<vaadin-combo-box-light <vaadin-combo-box-light
item-value-path="area_id" item-value-path="area_id"
item-id-path="area_id" item-id-path="area_id"
item-label-path="name" item-label-path="name"
.items=${this._areas} .items=${areas}
.value=${this._value} .value=${this._value}
.renderer=${rowRenderer} .renderer=${rowRenderer}
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@ -138,7 +323,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
</ha-icon-button> </ha-icon-button>
` `
: ""} : ""}
${this._areas.length > 0 ${areas.length > 0
? html` ? html`
<ha-icon-button <ha-icon-button
aria-label=${this.hass.localize( aria-label=${this.hass.localize(

View File

@ -11,6 +11,7 @@ import {
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import type { ToggleButton } from "../types"; import type { ToggleButton } from "../types";
import "./ha-svg-icon"; import "./ha-svg-icon";
import "@material/mwc-button/mwc-button";
@customElement("ha-button-toggle-group") @customElement("ha-button-toggle-group")
export class HaButtonToggleGroup extends LitElement { export class HaButtonToggleGroup extends LitElement {
@ -21,17 +22,22 @@ export class HaButtonToggleGroup extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div> <div>
${this.buttons.map( ${this.buttons.map((button) =>
(button) => html` button.iconPath
<mwc-icon-button ? html`<mwc-icon-button
.label=${button.label} .label=${button.label}
.value=${button.value} .value=${button.value}
?active=${this.active === button.value} ?active=${this.active === button.value}
@click=${this._handleClick} @click=${this._handleClick}
> >
<ha-svg-icon .path=${button.iconPath}></ha-svg-icon> <ha-svg-icon .path=${button.iconPath}></ha-svg-icon>
</mwc-icon-button> </mwc-icon-button>`
` : html`<mwc-button
.value=${button.value}
?active=${this.active === button.value}
@click=${this._handleClick}
>${button.label}</mwc-button
>`
)} )}
</div> </div>
`; `;
@ -49,13 +55,15 @@ export class HaButtonToggleGroup extends LitElement {
--mdc-icon-button-size: var(--button-toggle-size, 36px); --mdc-icon-button-size: var(--button-toggle-size, 36px);
--mdc-icon-size: var(--button-toggle-icon-size, 20px); --mdc-icon-size: var(--button-toggle-icon-size, 20px);
} }
mwc-icon-button { mwc-icon-button,
mwc-button {
border: 1px solid var(--primary-color); border: 1px solid var(--primary-color);
border-right-width: 0px; border-right-width: 0px;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
} }
mwc-icon-button::before { mwc-icon-button::before,
mwc-button::before {
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
@ -67,17 +75,21 @@ export class HaButtonToggleGroup extends LitElement {
content: ""; content: "";
transition: opacity 15ms linear, background-color 15ms linear; 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); 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; 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-radius: 0 4px 4px 0;
border-right-width: 1px; border-right-width: 1px;
} }
mwc-icon-button:only-child { mwc-icon-button:only-child,
mwc-button:only-child {
border-radius: 4px; border-radius: 4px;
border-right-width: 1px; border-right-width: 1px;
} }

View File

@ -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 { HomeAssistant } from "../../types";
import { AreaSelector } from "../../data/selector"; import { AreaSelector } from "../../data/selector";
import "../ha-area-picker"; 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") @customElement("ha-selector-area")
export class HaAreaSelector extends LitElement { export class HaAreaSelector extends LitElement {
@ -13,14 +22,76 @@ export class HaAreaSelector extends LitElement {
@property() public label?: string; @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() { protected render() {
return html`<ha-area-picker return html`<ha-area-picker
.hass=${this.hass} .hass=${this.hass}
.value=${this.value} .value=${this.value}
.label=${this.label} .label=${this.label}
no-add 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>`; ></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 { declare global {

View File

@ -19,13 +19,26 @@ export interface DeviceSelector {
integration?: string; integration?: string;
manufacturer?: string; manufacturer?: string;
model?: string; model?: string;
entity?: EntitySelector["entity"]; entity?: {
domain?: EntitySelector["entity"]["domain"];
device_class?: EntitySelector["entity"]["device_class"];
};
}; };
} }
export interface AreaSelector { 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 NumberSelector { export interface NumberSelector {