diff --git a/src/components/device/ha-device-multi-picker.ts b/src/components/device/ha-device-multi-picker.ts new file mode 100644 index 000000000000..c01e3ecd1f10 --- /dev/null +++ b/src/components/device/ha-device-multi-picker.ts @@ -0,0 +1,331 @@ +// @ts-ignore +import chipStyles from "@material/chips/dist/mdc.chips.min.css"; +import "@material/mwc-button/mwc-button"; +import "@polymer/paper-tooltip/paper-tooltip"; +import { mdiClose, mdiDevices, mdiPlus } from "@mdi/js"; +import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; +import { css, CSSResultGroup, html, LitElement, unsafeCSS } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import { fireEvent } from "../../common/dom/fire_event"; +import { ensureArray } from "../../common/ensure-array"; +import { + computeDeviceName, + DeviceRegistryEntry, + subscribeDeviceRegistry, +} from "../../data/device_registry"; +import { EntityRegistryEntry } from "../../data/entity_registry"; +import { SubscribeMixin } from "../../mixins/subscribe-mixin"; +import { HomeAssistant } from "../../types"; +import { HaDevicePickerDeviceFilterFunc } from "./ha-device-picker"; +import "../ha-icon-button"; +import "../ha-state-icon"; +import "../ha-svg-icon"; + +@customElement("ha-device-multi-picker") +export class HaDeviceMultiPicker extends SubscribeMixin(LitElement) { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public disabled?: boolean; + + @property() public label?: string; + + @property() public value?: any; + + /** + * Show only devices with entities from specific domains. + * @type {Array} + * @attr include-domains + */ + + @property({ type: Array, attribute: "include-domains" }) + public includeDomains?: string[]; + + /** + * Show no devices with entities of these domains. + * @type {Array} + * @attr exclude-domains + */ + @property({ type: Array, attribute: "exclude-domains" }) + public excludeDomains?: string[]; + + /** + * Show only devices 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; + + @property({ type: Boolean, attribute: "multiple" }) + public multiple?: boolean; + + @state() private _devices?: { [deviceId: string]: DeviceRegistryEntry }; + + @state() private _addMode?: "device_id"; + + @query("#input") private _inputElement?; + + public hassSubscribe(): UnsubscribeFunc[] { + return [ + subscribeDeviceRegistry(this.hass.connection!, (devices) => { + const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {}; + for (const device of devices) { + deviceLookup[device.id] = device; + } + this._devices = deviceLookup; + }), + ]; + } + + protected render() { + if (!this._devices) { + return html``; + } + return html` +
+ ${this.value + ? ensureArray(this.value).map((device_id) => { + const device = this._devices![device_id]; + return this._renderChip( + "device_id", + device_id, + device ? computeDeviceName(device, this.hass) : device_id, + undefined, + mdiDevices + ); + }) + : ""} +
+ ${this._renderPicker()} +
+
+
+ + + + ${this.hass.localize( + "ui.components.target-picker.add_device_id" + )} + + +
+
+ `; + } + + 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, + entityState?: HassEntity, + iconPath?: string + ) { + return html` +
+ ${iconPath + ? html`` + : ""} + ${entityState + ? html`` + : ""} + + + ${name} + + + + + ${this.hass.localize( + `ui.components.target-picker.remove_${type}` + )} + +
+ `; + } + + private _renderPicker() { + switch (this._addMode) { + case "device_id": + return html``; + } + return html``; + } + + private _targetPicked(ev) { + ev.stopPropagation(); + if (!ev.detail.value) { + return; + } + const value = + this.multiple && this.value + ? [...ensureArray(this.value), ev.detail.value] + : ev.detail.value; + const target = ev.currentTarget; + target.value = ""; + this._addMode = undefined; + fireEvent(this, "value-changed", { value }); + } + + private _handleRemove(ev) { + const target = ev.currentTarget as any; + fireEvent(this, "value-changed", { + value: this._removeItem(this.value, target.id), + }); + } + + private _removeItem(value: this["value"], id: string): this["value"] { + const newVal = ensureArray(value!)!.filter((val) => String(val) !== id); + if (newVal.length) { + return newVal; + } + return undefined; + } + + static get styles(): CSSResultGroup { + return css` + ${unsafeCSS(chipStyles)} + .mdc-chip { + color: var(--primary-text-color); + } + .items { + z-index: 2; + } + .mdc-chip-set { + padding: 4px 0; + } + .mdc-chip.add { + color: rgba(0, 0, 0, 0.87); + } + .mdc-chip:not(.add) { + cursor: default; + } + .mdc-chip ha-icon-button { + --mdc-icon-button-size: 24px; + display: flex; + align-items: center; + outline: none; + } + .mdc-chip ha-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(--secondary-text-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.expand { + min-width: 200px; + } + :host([disabled]) .mdc-chip { + opacity: var(--light-disabled-opacity); + pointer-events: none; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-device-multi-picker": HaDeviceMultiPicker; + } +} diff --git a/src/components/entity/ha-entity-multi-picker.ts b/src/components/entity/ha-entity-multi-picker.ts new file mode 100644 index 000000000000..c55c49988875 --- /dev/null +++ b/src/components/entity/ha-entity-multi-picker.ts @@ -0,0 +1,330 @@ +// @ts-ignore +import chipStyles from "@material/chips/dist/mdc.chips.min.css"; +import "@material/mwc-button/mwc-button"; +import "@polymer/paper-tooltip/paper-tooltip"; +import { mdiClose, mdiPlus } from "@mdi/js"; +import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; +import { css, CSSResultGroup, html, LitElement, unsafeCSS } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import { fireEvent } from "../../common/dom/fire_event"; +import { ensureArray } from "../../common/ensure-array"; +import { computeStateName } from "../../common/entity/compute_state_name"; +import { + EntityRegistryEntry, + subscribeEntityRegistry, +} from "../../data/entity_registry"; +import { SubscribeMixin } from "../../mixins/subscribe-mixin"; +import { HomeAssistant } from "../../types"; +import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker"; +import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker"; +import "../ha-icon-button"; +import "../ha-state-icon"; +import "../ha-svg-icon"; + +@customElement("ha-entity-multi-picker") +export class HaEntityMultiPicker extends SubscribeMixin(LitElement) { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public disabled?: boolean; + + @property() public label?: string; + + @property() public value?: any; + + /** + * Show only entitys with entities from specific domains. + * @type {Array} + * @attr include-domains + */ + + @property({ type: Array, attribute: "include-domains" }) + public includeDomains?: string[]; + + /** + * Show no entitys with entities of these domains. + * @type {Array} + * @attr exclude-domains + */ + @property({ type: Array, attribute: "exclude-domains" }) + public excludeDomains?: string[]; + + /** + * Show only entitys 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?: HaEntityPickerEntityFilterFunc; + + @property({ type: Boolean, attribute: "multiple" }) + public multiple?: boolean; + + @state() private _entities?: { [entityId: string]: EntityRegistryEntry }; + + @state() private _addMode?: "entity_id"; + + @query("#input") private _inputElement?; + + public hassSubscribe(): UnsubscribeFunc[] { + return [ + subscribeEntityRegistry(this.hass.connection!, (entities) => { + const entityLookup: { [entityId: string]: EntityRegistryEntry } = {}; + for (const entity of entities) { + entityLookup[entity.entity_id] = entity; + } + this._entities = entityLookup; + }), + ]; + } + + protected render() { + if (!this._entities) { + return html``; + } + return html` +
+ ${this.value + ? ensureArray(this.value).map((entity_id) => { + const entity = this.hass.states[entity_id]; + return this._renderChip( + "entity_id", + entity_id, + entity ? computeStateName(entity) : entity_id, + entity + ); + }) + : ""} +
+ ${this._renderPicker()} +
+
+
+ + + + ${this.hass.localize( + "ui.components.target-picker.add_entity_id" + )} + + +
+
+ `; + } + + 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, + entityState?: HassEntity, + iconPath?: string + ) { + return html` +
+ ${iconPath + ? html`` + : ""} + ${entityState + ? html`` + : ""} + + + ${name} + + + + + ${this.hass.localize( + `ui.components.target-picker.remove_${type}` + )} + +
+ `; + } + + private _renderPicker() { + switch (this._addMode) { + case "entity_id": + return html``; + } + return html``; + } + + private _targetPicked(ev) { + ev.stopPropagation(); + if (!ev.detail.value) { + return; + } + const value = + this.multiple && this.value + ? [...ensureArray(this.value), ev.detail.value] + : ev.detail.value; + const target = ev.currentTarget; + target.value = ""; + this._addMode = undefined; + fireEvent(this, "value-changed", { value }); + } + + private _handleRemove(ev) { + const target = ev.currentTarget as any; + fireEvent(this, "value-changed", { + value: this._removeItem(this.value, target.id), + }); + } + + private _removeItem(value: this["value"], id: string): this["value"] { + const newVal = ensureArray(value!)!.filter((val) => String(val) !== id); + if (newVal.length) { + return newVal; + } + return undefined; + } + + static get styles(): CSSResultGroup { + return css` + ${unsafeCSS(chipStyles)} + .mdc-chip { + color: var(--primary-text-color); + } + .items { + z-index: 2; + } + .mdc-chip-set { + padding: 4px 0; + } + .mdc-chip.add { + color: rgba(0, 0, 0, 0.87); + } + .mdc-chip:not(.add) { + cursor: default; + } + .mdc-chip ha-icon-button { + --mdc-icon-button-size: 24px; + display: flex; + align-items: center; + outline: none; + } + .mdc-chip ha-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(--secondary-text-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.expand { + min-width: 200px; + } + :host([disabled]) .mdc-chip { + opacity: var(--light-disabled-opacity); + pointer-events: none; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-entity-multi-picker": HaEntityMultiPicker; + } +} diff --git a/src/components/ha-area-multi-picker.ts b/src/components/ha-area-multi-picker.ts new file mode 100644 index 000000000000..9b0a60ca977b --- /dev/null +++ b/src/components/ha-area-multi-picker.ts @@ -0,0 +1,331 @@ +// @ts-ignore +import chipStyles from "@material/chips/dist/mdc.chips.min.css"; +import "@material/mwc-button/mwc-button"; +import "@polymer/paper-tooltip/paper-tooltip"; +import { mdiClose, mdiPlus, mdiSofa } from "@mdi/js"; +import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; +import { css, CSSResultGroup, html, LitElement, unsafeCSS } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import { fireEvent } from "../common/dom/fire_event"; +import { ensureArray } from "../common/ensure-array"; +import { + AreaRegistryEntry, + subscribeAreaRegistry, +} from "../data/area_registry"; +import { EntityRegistryEntry } from "../data/entity_registry"; +import { SubscribeMixin } from "../mixins/subscribe-mixin"; +import { HomeAssistant } from "../types"; +import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; +import "./ha-area-picker"; +import "./ha-icon-button"; +import "./ha-state-icon"; +import "./ha-svg-icon"; + +@customElement("ha-area-multi-picker") +export class HaAreaMultiPicker extends SubscribeMixin(LitElement) { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public disabled?: boolean; + + @property() public label?: string; + + @property() public value?: any; + + /** + * 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; + + @property({ type: Boolean, attribute: "multiple" }) + public multiple?: boolean; + + @state() private _areas?: { [areaId: string]: AreaRegistryEntry }; + + @state() private _addMode?: "area_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; + }), + ]; + } + + protected render() { + if (!this._areas) { + return html``; + } + return html` +
+ ${this.value + ? ensureArray(this.value).map((area_id) => { + const area = this._areas![area_id]; + return this._renderChip( + "area_id", + area_id, + area?.name || area_id, + undefined, + mdiSofa + ); + }) + : ""} +
+ ${this._renderPicker()} +
+
+
+ + + + ${this.hass.localize( + "ui.components.target-picker.add_area_id" + )} + + +
+
+ `; + } + + 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, + entityState?: HassEntity, + iconPath?: string + ) { + return html` +
+ ${iconPath + ? html`` + : ""} + ${entityState + ? html`` + : ""} + + + ${name} + + + + + ${this.hass.localize( + `ui.components.target-picker.remove_${type}` + )} + +
+ `; + } + + private _renderPicker() { + switch (this._addMode) { + case "area_id": + return html``; + } + return html``; + } + + private _targetPicked(ev) { + ev.stopPropagation(); + if (!ev.detail.value) { + return; + } + const value = + this.multiple && this.value + ? [...ensureArray(this.value), ev.detail.value] + : ev.detail.value; + const target = ev.currentTarget; + target.value = ""; + this._addMode = undefined; + fireEvent(this, "value-changed", { value }); + } + + private _handleRemove(ev) { + const target = ev.currentTarget as any; + fireEvent(this, "value-changed", { + value: this._removeItem(this.value, target.id), + }); + } + + private _removeItem(value: this["value"], id: string): this["value"] { + const newVal = ensureArray(value!)!.filter((val) => String(val) !== id); + if (newVal.length) { + return newVal; + } + return undefined; + } + + static get styles(): CSSResultGroup { + return css` + ${unsafeCSS(chipStyles)} + .mdc-chip { + color: var(--primary-text-color); + } + .items { + z-index: 2; + } + .mdc-chip-set { + padding: 4px 0; + } + .mdc-chip.add { + color: rgba(0, 0, 0, 0.87); + } + .mdc-chip:not(.add) { + cursor: default; + } + .mdc-chip ha-icon-button { + --mdc-icon-button-size: 24px; + display: flex; + align-items: center; + outline: none; + } + .mdc-chip ha-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(--secondary-text-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.expand { + min-width: 200px; + } + :host([disabled]) .mdc-chip { + opacity: var(--light-disabled-opacity); + pointer-events: none; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-area-multi-picker": HaAreaMultiPicker; + } +} diff --git a/src/components/ha-selector/ha-selector-area.ts b/src/components/ha-selector/ha-selector-area.ts index 5f307dd0130d..097f90a7735b 100644 --- a/src/components/ha-selector/ha-selector-area.ts +++ b/src/components/ha-selector/ha-selector-area.ts @@ -5,7 +5,7 @@ import { DeviceRegistryEntry } from "../../data/device_registry"; import { EntityRegistryEntry } from "../../data/entity_registry"; import { AreaSelector } from "../../data/selector"; import { HomeAssistant } from "../../types"; -import "../ha-area-picker"; +import "../ha-area-multi-picker"; @customElement("ha-selector-area") export class HaAreaSelector extends LitElement { @@ -34,11 +34,10 @@ export class HaAreaSelector extends LitElement { } protected render() { - return html``; + .multiple=${this.selector.area.multiple} + >`; } private _filterEntities = (entity: EntityRegistryEntry): boolean => { diff --git a/src/components/ha-selector/ha-selector-device.ts b/src/components/ha-selector/ha-selector-device.ts index 60da624665bd..7d74352c1107 100644 --- a/src/components/ha-selector/ha-selector-device.ts +++ b/src/components/ha-selector/ha-selector-device.ts @@ -4,7 +4,7 @@ import { ConfigEntry, getConfigEntries } from "../../data/config_entries"; import { DeviceRegistryEntry } from "../../data/device_registry"; import { DeviceSelector } from "../../data/selector"; import { HomeAssistant } from "../../types"; -import "../device/ha-device-picker"; +import "../device/ha-device-multi-picker"; @customElement("ha-selector-device") export class HaDeviceSelector extends LitElement { @@ -30,7 +30,7 @@ export class HaDeviceSelector extends LitElement { } protected render() { - return html``; + .multiple=${this.selector.device.multiple} + >`; } private _filterDevices = (device: DeviceRegistryEntry): boolean => { diff --git a/src/components/ha-selector/ha-selector-entity.ts b/src/components/ha-selector/ha-selector-entity.ts index 2facd604babe..3028940ada9d 100644 --- a/src/components/ha-selector/ha-selector-entity.ts +++ b/src/components/ha-selector/ha-selector-entity.ts @@ -6,7 +6,7 @@ import { subscribeEntityRegistry } from "../../data/entity_registry"; import { EntitySelector } from "../../data/selector"; import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../types"; -import "../entity/ha-entity-picker"; +import "../entity/ha-entity-multi-picker"; @customElement("ha-selector-entity") export class HaEntitySelector extends SubscribeMixin(LitElement) { @@ -23,14 +23,15 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { @property({ type: Boolean }) public disabled = false; protected render() { - return html``; + .multiple=${this.selector.entity.multiple} + >`; } public hassSubscribe(): UnsubscribeFunc[] { diff --git a/src/data/selector.ts b/src/data/selector.ts index 3bb2999877b8..39839c96676b 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -16,6 +16,7 @@ export interface EntitySelector { integration?: string; domain?: string; device_class?: string; + multiple?: boolean; }; } @@ -28,6 +29,7 @@ export interface DeviceSelector { domain?: EntitySelector["entity"]["domain"]; device_class?: EntitySelector["entity"]["device_class"]; }; + multiple?: boolean; }; } @@ -50,6 +52,7 @@ export interface AreaSelector { manufacturer?: DeviceSelector["device"]["manufacturer"]; model?: DeviceSelector["device"]["model"]; }; + multiple?: boolean; }; }