From fe31d15d27ce08ca8183d73a44b580182d5063e0 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sun, 29 Nov 2020 22:00:51 +0100 Subject: [PATCH] Add UI for setting an area on entity level (#7837) --- .../dialogs/network/dialog-hassio-network.ts | 10 +- src/components/ha-area-picker.ts | 12 +++ src/components/ha-expansion-panel.ts | 6 +- src/data/entity_registry.ts | 2 + .../blueprint/dialog-import-blueprint.ts | 23 ++--- .../device-detail/ha-device-info-card.ts | 2 +- .../dialog-device-registry-detail.ts | 14 +-- .../show-dialog-device-registry-detail.ts | 4 +- .../config/devices/ha-config-device-page.ts | 2 +- .../entities/entity-registry-basic-editor.ts | 50 +++++++++- .../entities/entity-registry-settings.ts | 95 ++++++++++++++++++- .../config/entities/ha-config-entities.ts | 68 ++++++++++++- src/translations/en.json | 1 + 13 files changed, 246 insertions(+), 43 deletions(-) rename src/{dialogs => panels/config/devices}/device-registry-detail/dialog-device-registry-detail.ts (90%) rename src/{dialogs => panels/config/devices}/device-registry-detail/show-dialog-device-registry-detail.ts (86%) diff --git a/hassio/src/dialogs/network/dialog-hassio-network.ts b/hassio/src/dialogs/network/dialog-hassio-network.ts index 382cc0ede3..6a8deb6088 100644 --- a/hassio/src/dialogs/network/dialog-hassio-network.ts +++ b/hassio/src/dialogs/network/dialog-hassio-network.ts @@ -137,8 +137,7 @@ export class DialogHassioNetwork extends LitElement )} ${this._interface?.type === "wireless" ? html` - - Wi-Fi + ${this._interface?.wifi?.ssid ? html`

Connected to: ${this._interface?.wifi?.ssid}

` : ""} @@ -281,8 +280,10 @@ export class DialogHassioNetwork extends LitElement private _renderIPConfiguration(version: string) { return html` - - IPv${version.charAt(version.length - 1)} +
{ + return this._areas?.find((area) => area.area_id === areaId); + }); + private _clearValue(ev: Event) { ev.stopPropagation(); this._setValue(""); diff --git a/src/components/ha-expansion-panel.ts b/src/components/ha-expansion-panel.ts index b74022032d..f328e3590e 100644 --- a/src/components/ha-expansion-panel.ts +++ b/src/components/ha-expansion-panel.ts @@ -19,12 +19,14 @@ class HaExpansionPanel extends LitElement { @property({ type: Boolean, reflect: true }) outlined = false; + @property() header?: string; + @query(".container") private _container!: HTMLDivElement; protected render(): TemplateResult { return html`
- + ${this.header} `} - - ${this.hass.localize( - "ui.panel.config.blueprint.add.raw_blueprint" - )} +
${this._result.raw_data}
` : html`${this.hass.localize( @@ -201,15 +199,8 @@ class DialogImportBlueprint extends LitElement { } } - static get styles(): CSSResult[] { - return [ - haStyleDialog, - css` - ha-expansion-panel { - --expansion-panel-summary-padding: 0; - } - `, - ]; + static get styles(): CSSResult { + return haStyleDialog; } } diff --git a/src/panels/config/devices/device-detail/ha-device-info-card.ts b/src/panels/config/devices/device-detail/ha-device-info-card.ts index ae81de81c2..5e4de8859e 100644 --- a/src/panels/config/devices/device-detail/ha-device-info-card.ts +++ b/src/panels/config/devices/device-detail/ha-device-info-card.ts @@ -12,7 +12,7 @@ import { computeDeviceName, DeviceRegistryEntry, } from "../../../../data/device_registry"; -import { loadDeviceRegistryDetailDialog } from "../../../../dialogs/device-registry-detail/show-dialog-device-registry-detail"; +import { loadDeviceRegistryDetailDialog } from "../device-registry-detail/show-dialog-device-registry-detail"; import { HomeAssistant } from "../../../../types"; @customElement("ha-device-info-card") diff --git a/src/dialogs/device-registry-detail/dialog-device-registry-detail.ts b/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts similarity index 90% rename from src/dialogs/device-registry-detail/dialog-device-registry-detail.ts rename to src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts index 4bc68af3a2..ba05ad3c66 100644 --- a/src/dialogs/device-registry-detail/dialog-device-registry-detail.ts +++ b/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts @@ -3,8 +3,8 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-input/paper-input"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; -import "../../components/ha-dialog"; -import "../../components/ha-area-picker"; +import "../../../../components/ha-dialog"; +import "../../../../components/ha-area-picker"; import { CSSResult, @@ -18,11 +18,11 @@ import { } from "lit-element"; import { DeviceRegistryDetailDialogParams } from "./show-dialog-device-registry-detail"; -import { HomeAssistant } from "../../types"; -import { PolymerChangedEvent } from "../../polymer-types"; -import { computeDeviceName } from "../../data/device_registry"; -import { fireEvent } from "../../common/dom/fire_event"; -import { haStyleDialog } from "../../resources/styles"; +import { HomeAssistant } from "../../../../types"; +import { PolymerChangedEvent } from "../../../../polymer-types"; +import { computeDeviceName } from "../../../../data/device_registry"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { haStyleDialog } from "../../../../resources/styles"; @customElement("dialog-device-registry-detail") class DialogDeviceRegistryDetail extends LitElement { diff --git a/src/dialogs/device-registry-detail/show-dialog-device-registry-detail.ts b/src/panels/config/devices/device-registry-detail/show-dialog-device-registry-detail.ts similarity index 86% rename from src/dialogs/device-registry-detail/show-dialog-device-registry-detail.ts rename to src/panels/config/devices/device-registry-detail/show-dialog-device-registry-detail.ts index 4a603f629c..383446906b 100644 --- a/src/dialogs/device-registry-detail/show-dialog-device-registry-detail.ts +++ b/src/panels/config/devices/device-registry-detail/show-dialog-device-registry-detail.ts @@ -1,8 +1,8 @@ -import { fireEvent } from "../../common/dom/fire_event"; +import { fireEvent } from "../../../../common/dom/fire_event"; import { DeviceRegistryEntry, DeviceRegistryEntryMutableParams, -} from "../../data/device_registry"; +} from "../../../../data/device_registry"; export interface DeviceRegistryDetailDialogParams { device: DeviceRegistryEntry; diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index a8809ec5f9..84ed1860c5 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -35,7 +35,7 @@ import { findRelated, RelatedResult } from "../../../data/search"; import { loadDeviceRegistryDetailDialog, showDeviceRegistryDetailDialog, -} from "../../../dialogs/device-registry-detail/show-dialog-device-registry-detail"; +} from "./device-registry-detail/show-dialog-device-registry-detail"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-error-screen"; import "../../../layouts/hass-tabs-subpage"; diff --git a/src/panels/config/entities/entity-registry-basic-editor.ts b/src/panels/config/entities/entity-registry-basic-editor.ts index 79d4bcb07b..f40044f1b0 100644 --- a/src/panels/config/entities/entity-registry-basic-editor.ts +++ b/src/panels/config/entities/entity-registry-basic-editor.ts @@ -20,9 +20,16 @@ import { import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import type { PolymerChangedEvent } from "../../../polymer-types"; import type { HomeAssistant } from "../../../types"; +import "../../../components/ha-area-picker"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { + DeviceRegistryEntry, + subscribeDeviceRegistry, +} from "../../../data/device_registry"; +import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; @customElement("ha-registry-basic-editor") -export class HaEntityRegistryBasicEditor extends LitElement { +export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @property() public entry!: ExtEntityRegistryEntry; @@ -31,16 +38,26 @@ export class HaEntityRegistryBasicEditor extends LitElement { @internalProperty() private _entityId!: string; + @internalProperty() private _areaId?: string; + @internalProperty() private _disabledBy!: string | null; + private _deviceLookup?: Record; + + @internalProperty() private _device?: DeviceRegistryEntry; + @internalProperty() private _submitting?: boolean; public async updateEntry(): Promise { this._submitting = true; const params: Partial = { new_entity_id: this._entityId.trim(), + area_id: this._areaId || null, }; - if (this._disabledBy === null || this._disabledBy === "user") { + if ( + this.entry.disabled_by !== this._disabledBy && + (this._disabledBy === null || this._disabledBy === "user") + ) { params.disabled_by = this._disabledBy; } try { @@ -70,6 +87,20 @@ export class HaEntityRegistryBasicEditor extends LitElement { } } + public hassSubscribe(): UnsubscribeFunc[] { + return [ + subscribeDeviceRegistry(this.hass.connection!, (devices) => { + this._deviceLookup = {}; + for (const device of devices) { + this._deviceLookup[device.id] = device; + } + if (!this._device && this.entry.device_id) { + this._device = this._deviceLookup[this.entry.device_id]; + } + }), + ]; + } + protected updated(changedProperties: PropertyValues) { super.updated(changedProperties); if (!changedProperties.has("entry")) { @@ -79,6 +110,11 @@ export class HaEntityRegistryBasicEditor extends LitElement { this._origEntityId = this.entry.entity_id; this._entityId = this.entry.entity_id; this._disabledBy = this.entry.disabled_by; + this._areaId = this.entry.area_id; + this._device = + this.entry.device_id && this._deviceLookup + ? this._deviceLookup[this.entry.device_id] + : undefined; } } @@ -105,6 +141,12 @@ export class HaEntityRegistryBasicEditor extends LitElement { .invalid=${invalidDomainUpdate} .disabled=${this._submitting} > +
): void { this._entityId = ev.detail.value; } diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 0912c6331f..b15bf6bffb 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -1,6 +1,6 @@ import "@material/mwc-button/mwc-button"; import "@polymer/paper-input/paper-input"; -import { HassEntity } from "home-assistant-js-websocket"; +import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResult, @@ -31,9 +31,18 @@ import type { PolymerChangedEvent } from "../../../polymer-types"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { domainIcon } from "../../../common/entity/domain_icon"; +import "../../../components/ha-area-picker"; +import { + DeviceRegistryEntry, + subscribeDeviceRegistry, + updateDeviceRegistryEntry, +} from "../../../data/device_registry"; +import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; +import "../../../components/ha-expansion-panel"; +import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail"; @customElement("entity-registry-settings") -export class EntityRegistrySettings extends LitElement { +export class EntityRegistrySettings extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @property() public entry!: ExtEntityRegistryEntry; @@ -44,14 +53,34 @@ export class EntityRegistrySettings extends LitElement { @internalProperty() private _entityId!: string; + @internalProperty() private _areaId?: string | null; + @internalProperty() private _disabledBy!: string | null; + private _deviceLookup?: Record; + + @internalProperty() private _device?: DeviceRegistryEntry; + @internalProperty() private _error?: string; @internalProperty() private _submitting?: boolean; private _origEntityId!: string; + public hassSubscribe(): UnsubscribeFunc[] { + return [ + subscribeDeviceRegistry(this.hass.connection!, (devices) => { + this._deviceLookup = {}; + for (const device of devices) { + this._deviceLookup[device.id] = device; + } + if (this.entry.device_id) { + this._device = this._deviceLookup[this.entry.device_id]; + } + }), + ]; + } + protected updated(changedProperties: PropertyValues) { super.updated(changedProperties); if (changedProperties.has("entry")) { @@ -59,8 +88,13 @@ export class EntityRegistrySettings extends LitElement { this._name = this.entry.name || ""; this._icon = this.entry.icon || ""; this._origEntityId = this.entry.entity_id; + this._areaId = this.entry.area_id; this._entityId = this.entry.entity_id; this._disabledBy = this.entry.disabled_by; + this._device = + this.entry.device_id && this._deviceLookup + ? this._deviceLookup[this.entry.device_id] + : undefined; } } @@ -117,6 +151,13 @@ export class EntityRegistrySettings extends LitElement { .invalid=${invalidDomainUpdate} .disabled=${this._submitting} > + ${!this.entry.device_id + ? html`` + : ""}
+ + ${this.entry.device_id + ? html` +

+ By default the entities of a device are in the same area as the + device. If you change the area of this entity, it will no longer + follow the area of the device. +

+ ${this._areaId + ? html`Follow device area` + : this._device + ? html`Change device area` + : ""} +
` + : ""}
{ + await updateDeviceRegistryEntry(this.hass, this._device!.id, updates); + }, + }); + } + private async _updateEntry(): Promise { this._submitting = true; const params: Partial = { name: this._name.trim() || null, icon: this._icon.trim() || null, + area_id: this._areaId || null, new_entity_id: this._entityId.trim(), }; - if (this._disabledBy === null || this._disabledBy === "user") { + if ( + this.entry.disabled_by !== this._disabledBy && + (this._disabledBy === null || this._disabledBy === "user") + ) { params.disabled_by = this._disabledBy; } try { diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 3a474c4efb..b8e0ee77da 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -62,6 +62,14 @@ import { } from "./show-dialog-entity-editor"; import { haStyle } from "../../../resources/styles"; import { UNAVAILABLE } from "../../../data/entity"; +import { + DeviceRegistryEntry, + subscribeDeviceRegistry, +} from "../../../data/device_registry"; +import { + AreaRegistryEntry, + subscribeAreaRegistry, +} from "../../../data/area_registry"; export interface StateEntity extends EntityRegistryEntry { readonly?: boolean; @@ -73,6 +81,7 @@ export interface EntityRow extends StateEntity { unavailable: boolean; restored: boolean; status: string; + area?: string; } @customElement("ha-config-entities") @@ -87,6 +96,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { @internalProperty() private _entities?: EntityRegistryEntry[]; + @internalProperty() private _devices?: DeviceRegistryEntry[]; + + @internalProperty() private _areas: AreaRegistryEntry[] = []; + @internalProperty() private _stateEntities: StateEntity[] = []; @property() public _entries?: ConfigEntry[]; @@ -201,6 +214,15 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { template: (platform) => this.hass.localize(`component.${platform}.title`) || platform, }, + area: { + title: this.hass.localize( + "ui.panel.config.entities.picker.headers.area" + ), + sortable: true, + hidden: narrow, + filterable: true, + width: "15%", + }, status: { title: this.hass.localize( "ui.panel.config.entities.picker.headers.status" @@ -255,6 +277,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { private _filteredEntities = memoize( ( entities: EntityRegistryEntry[], + devices: DeviceRegistryEntry[] | undefined, + areas: AreaRegistryEntry[] | undefined, stateEntities: StateEntity[], filters: URLSearchParams, showDisabled: boolean, @@ -262,21 +286,42 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { showReadOnly: boolean ): EntityRow[] => { const result: EntityRow[] = []; + // If nothing gets filtered, this is our correct count of entities let startLength = entities.length + stateEntities.length; - entities = showReadOnly ? entities.concat(stateEntities) : entities; + const areaLookup: { [areaId: string]: AreaRegistryEntry } = {}; + const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {}; + + if (areas) { + for (const area of areas) { + areaLookup[area.area_id] = area; + } + if (devices) { + for (const device of devices) { + deviceLookup[device.id] = device; + } + } + } + + entities.forEach((entity) => { + return entity; + }); + + let filteredEntities = showReadOnly + ? entities.concat(stateEntities) + : entities; filters.forEach((value, key) => { switch (key) { case "config_entry": - entities = entities.filter( + filteredEntities = filteredEntities.filter( (entity) => entity.config_entry_id === value ); // If we have an active filter and `showReadOnly` is true, the length of `entities` is correct. // If however, the read-only entities were not added before, we need to check how many would // have matched the active filter and add that number to the count. - startLength = entities.length; + startLength = filteredEntities.length; if (!showReadOnly) { startLength += stateEntities.filter( (entity) => entity.config_entry_id === value @@ -287,13 +332,17 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { }); if (!showDisabled) { - entities = entities.filter((entity) => !entity.disabled_by); + filteredEntities = filteredEntities.filter( + (entity) => !entity.disabled_by + ); } - for (const entry of entities) { + for (const entry of filteredEntities) { const entity = this.hass.states[entry.entity_id]; const unavailable = entity?.state === UNAVAILABLE; const restored = entity?.attributes.restored; + const areaId = entry.area_id ?? deviceLookup[entry.device_id!]?.area_id; + const area = areaId ? areaLookup[areaId] : undefined; if (!showUnavailable && unavailable) { continue; @@ -309,6 +358,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { this.hass.localize("state.default.unavailable"), unavailable, restored, + area: area ? area.name : undefined, status: restored ? this.hass.localize( "ui.panel.config.entities.picker.status.restored" @@ -345,6 +395,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { subscribeEntityRegistry(this.hass.connection!, (entities) => { this._entities = entities; }), + subscribeDeviceRegistry(this.hass.connection!, (devices) => { + this._devices = devices; + }), + subscribeAreaRegistry(this.hass.connection, (areas) => { + this._areas = areas; + }), ]; } @@ -372,6 +428,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { const entityData = this._filteredEntities( this._entities, + this._devices, + this._areas, this._stateEntities, this._searchParms, this._showDisabled, diff --git a/src/translations/en.json b/src/translations/en.json index df894c5333..eceb15c8e3 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1830,6 +1830,7 @@ "name": "Name", "entity_id": "Entity ID", "integration": "Integration", + "area": "Area", "status": "Status" }, "selected": "{number} selected",