From f4859320ebadd701ee0aa4df6d560a367e43bf73 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 31 Jan 2024 14:18:43 +0100 Subject: [PATCH] Add icon to areas (#19585) * Add icon to areas * Fix gallery --------- Co-authored-by: Paul Bottein --- gallery/src/pages/components/ha-form.ts | 6 +- gallery/src/pages/components/ha-selector.ts | 6 +- src/components/ha-area-picker.ts | 13 +- src/components/ha-target-picker.ts | 20 +- src/data/area_registry.ts | 2 + src/data/ws-area_registry.ts | 15 +- .../areas/dialog-area-registry-detail.ts | 27 ++- .../config/areas/ha-config-area-page.ts | 66 ++---- .../config/areas/ha-config-areas-dashboard.ts | 188 +++++++++--------- src/translations/en.json | 1 + 10 files changed, 176 insertions(+), 168 deletions(-) diff --git a/gallery/src/pages/components/ha-form.ts b/gallery/src/pages/components/ha-form.ts index dc8fdb5c51..391b01c210 100644 --- a/gallery/src/pages/components/ha-form.ts +++ b/gallery/src/pages/components/ha-form.ts @@ -10,6 +10,7 @@ import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervis import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data"; import "../../../../src/components/ha-form/ha-form"; import type { HaFormSchema } from "../../../../src/components/ha-form/types"; +import type { AreaRegistryEntry } from "../../../../src/data/area_registry"; import { getEntity } from "../../../../src/fake_data/entity"; import { provideHass } from "../../../../src/fake_data/provide_hass"; import { HomeAssistant } from "../../../../src/types"; @@ -97,22 +98,25 @@ const DEVICES = [ }, ]; -const AREAS = [ +const AREAS: AreaRegistryEntry[] = [ { area_id: "backyard", name: "Backyard", + icon: null, picture: null, aliases: [], }, { area_id: "bedroom", name: "Bedroom", + icon: "mdi:bed", picture: null, aliases: [], }, { area_id: "livingroom", name: "Livingroom", + icon: "mdi:sofa", picture: null, aliases: [], }, diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index 004bb3f406..78dbe681bb 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -9,6 +9,7 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry"; import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; import "../../../../src/components/ha-selector/ha-selector"; import "../../../../src/components/ha-settings-row"; +import type { AreaRegistryEntry } from "../../../../src/data/area_registry"; import { BlueprintInput } from "../../../../src/data/blueprint"; import { showDialog } from "../../../../src/dialogs/make-dialog-manager"; import { getEntity } from "../../../../src/fake_data/entity"; @@ -93,22 +94,25 @@ const DEVICES = [ }, ]; -const AREAS = [ +const AREAS: AreaRegistryEntry[] = [ { area_id: "backyard", name: "Backyard", + icon: null, picture: null, aliases: [], }, { area_id: "bedroom", name: "Bedroom", + icon: "mdi:bed", picture: null, aliases: [], }, { area_id: "livingroom", name: "Livingroom", + icon: "mdi:sofa", picture: null, aliases: [], }, diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts index e3b237e8e7..b68b08039a 100644 --- a/src/components/ha-area-picker.ts +++ b/src/components/ha-area-picker.ts @@ -1,6 +1,6 @@ import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { HassEntity } from "home-assistant-js-websocket"; -import { html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { html, LitElement, nothing, PropertyValues, TemplateResult } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; @@ -36,8 +36,12 @@ type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry; const rowRenderer: ComboBoxLitRenderer = (item) => html` + ${item.icon + ? html`` + : nothing} ${item.name} `; @@ -135,6 +139,7 @@ export class HaAreaPicker extends LitElement { area_id: "no_areas", name: this.hass.localize("ui.components.area-picker.no_areas"), picture: null, + icon: null, aliases: [], }, ]; @@ -262,7 +267,9 @@ export class HaAreaPicker extends LitElement { } if (areaIds) { - outputAreas = areas.filter((area) => areaIds!.includes(area.area_id)); + outputAreas = outputAreas.filter((area) => + areaIds!.includes(area.area_id) + ); } if (excludeAreas) { @@ -277,6 +284,7 @@ export class HaAreaPicker extends LitElement { area_id: "no_areas", name: this.hass.localize("ui.components.area-picker.no_match"), picture: null, + icon: null, aliases: [], }, ]; @@ -290,6 +298,7 @@ export class HaAreaPicker extends LitElement { area_id: "add_new", name: this.hass.localize("ui.components.area-picker.add_new"), picture: null, + icon: "mdi:plus", aliases: [], }, ]; diff --git a/src/components/ha-target-picker.ts b/src/components/ha-target-picker.ts index d0ed84559e..4030e8ff34 100644 --- a/src/components/ha-target-picker.ts +++ b/src/components/ha-target-picker.ts @@ -98,6 +98,7 @@ export class HaTargetPicker extends LitElement { area_id, area?.name || area_id, undefined, + area?.icon, mdiSofa ); }) @@ -110,6 +111,7 @@ export class HaTargetPicker extends LitElement { device_id, device ? computeDeviceName(device, this.hass) : device_id, undefined, + undefined, mdiDevices ); }) @@ -209,7 +211,8 @@ export class HaTargetPicker extends LitElement { id: string, name: string, entityState?: HassEntity, - iconPath?: string + icon?: string | null, + fallbackIconPath?: string ) { return html`
- ${iconPath - ? html`` - : ""} + .icon=${icon} + >` + : fallbackIconPath + ? html`` + : ""} ${entityState ? html` - conn - .sendMessagePromise({ - type: "config/area_registry/list", - }) - .then((areas) => - (areas as AreaRegistryEntry[]).sort((ent1, ent2) => - stringCompare(ent1.name, ent2.name) - ) - ); + conn.sendMessagePromise({ + type: "config/area_registry/list", + }); const subscribeAreaRegistryUpdates = ( conn: Connection, diff --git a/src/panels/config/areas/dialog-area-registry-detail.ts b/src/panels/config/areas/dialog-area-registry-detail.ts index 9302f45a98..fafeecd787 100644 --- a/src/panels/config/areas/dialog-area-registry-detail.ts +++ b/src/panels/config/areas/dialog-area-registry-detail.ts @@ -9,6 +9,7 @@ import { createCloseHeading } from "../../../components/ha-dialog"; import "../../../components/ha-picture-upload"; import type { HaPictureUpload } from "../../../components/ha-picture-upload"; import "../../../components/ha-settings-row"; +import "../../../components/ha-icon-picker"; import "../../../components/ha-textfield"; import { AreaRegistryEntryMutableParams } from "../../../data/area_registry"; import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog"; @@ -32,6 +33,8 @@ class DialogAreaDetail extends LitElement { @state() private _picture!: string | null; + @state() private _icon!: string | null; + @state() private _error?: string; @state() private _params?: AreaRegistryDetailDialogParams; @@ -46,6 +49,7 @@ class DialogAreaDetail extends LitElement { this._name = this._params.entry ? this._params.entry.name : ""; this._aliases = this._params.entry ? this._params.entry.aliases : []; this._picture = this._params.entry?.picture || null; + this._icon = this._params.entry?.icon || null; await this.updateComplete; } @@ -101,6 +105,13 @@ class DialogAreaDetail extends LitElement { dialogInitialFocus > + + ) { this._error = undefined; this._picture = (ev.target as HaPictureUpload).value; } private async _updateEntry() { + const create = !this._params!.entry; this._submitting = true; try { const values: AreaRegistryEntryMutableParams = { name: this._name.trim(), - picture: this._picture, + picture: this._picture || (create ? undefined : null), + icon: this._icon || (create ? undefined : null), aliases: this._aliases, }; - if (this._params!.entry) { - await this._params!.updateEntry!(values); - } else { + if (create) { await this._params!.createEntry!(values); + } else { + await this._params!.updateEntry!(values); } this.closeDialog(); } catch (err: any) { @@ -189,6 +207,7 @@ class DialogAreaDetail extends LitElement { haStyleDialog, css` ha-textfield, + ha-icon-picker, ha-picture-upload { display: block; margin-bottom: 16px; diff --git a/src/panels/config/areas/ha-config-area-page.ts b/src/panels/config/areas/ha-config-area-page.ts index dd35c763b7..62abd48d4e 100644 --- a/src/panels/config/areas/ha-config-area-page.ts +++ b/src/panels/config/areas/ha-config-area-page.ts @@ -1,11 +1,9 @@ +import { consume } from "@lit-labs/context"; import "@material/mwc-button"; import "@material/mwc-list"; import { mdiDelete, mdiDotsVertical, mdiImagePlus, mdiPencil } from "@mdi/js"; -import { - HassEntity, - UnsubscribeFunc, -} from "home-assistant-js-websocket/dist/types"; -import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { HassEntity } from "home-assistant-js-websocket/dist/types"; +import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import memoizeOne from "memoize-one"; @@ -18,33 +16,31 @@ import { afterNextRender } from "../../../common/util/render-status"; import "../../../components/ha-card"; import "../../../components/ha-icon-button"; import "../../../components/ha-icon-next"; +import "../../../components/ha-list-item"; import { AreaRegistryEntry, deleteAreaRegistryEntry, - subscribeAreaRegistry, updateAreaRegistryEntry, } from "../../../data/area_registry"; import { AutomationEntity } from "../../../data/automation"; +import { fullEntitiesContext } from "../../../data/context"; import { - computeDeviceName, DeviceRegistryEntry, + computeDeviceName, sortDeviceRegistryByName, - subscribeDeviceRegistry, } from "../../../data/device_registry"; import { - computeEntityRegistryName, EntityRegistryEntry, + computeEntityRegistryName, sortEntityRegistryByName, - subscribeEntityRegistry, } from "../../../data/entity_registry"; import { SceneEntity } from "../../../data/scene"; import { ScriptEntity } from "../../../data/script"; -import { findRelated, RelatedResult } from "../../../data/search"; +import { RelatedResult, findRelated } from "../../../data/search"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog"; import "../../../layouts/hass-error-screen"; import "../../../layouts/hass-subpage"; -import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import "../../logbook/ha-logbook"; @@ -52,7 +48,6 @@ import { loadAreaRegistryDetailDialog, showAreaRegistryDetailDialog, } from "./show-dialog-area-registry-detail"; -import "../../../components/ha-list-item"; declare type NameAndEntity = { name: string; @@ -60,7 +55,7 @@ declare type NameAndEntity = { }; @customElement("ha-config-area-page") -class HaConfigAreaPage extends SubscribeMixin(LitElement) { +class HaConfigAreaPage extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property() public areaId!: string; @@ -71,24 +66,14 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) { @property({ type: Boolean }) public showAdvanced = false; - @state() public _areas!: AreaRegistryEntry[]; - - @state() public _devices!: DeviceRegistryEntry[]; - - @state() public _entities!: EntityRegistryEntry[]; + @state() + @consume({ context: fullEntitiesContext, subscribe: true }) + _entityReg!: EntityRegistryEntry[]; @state() private _related?: RelatedResult; private _logbookTime = { recent: 86400 }; - private _area = memoizeOne( - ( - areaId: string, - areas: AreaRegistryEntry[] - ): AreaRegistryEntry | undefined => - areas.find((area) => area.area_id === areaId) - ); - private _memberships = memoizeOne( ( areaId: string, @@ -150,26 +135,12 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) { } } - protected hassSubscribe(): (UnsubscribeFunc | Promise)[] { - return [ - subscribeAreaRegistry(this.hass.connection, (areas) => { - this._areas = areas; - }), - subscribeDeviceRegistry(this.hass.connection, (entries) => { - this._devices = entries; - }), - subscribeEntityRegistry(this.hass.connection, (entries) => { - this._entities = entries; - }), - ]; - } - protected render() { - if (!this._areas || !this._devices || !this._entities) { + if (!this.hass.areas || !this.hass.devices || !this.hass.entities) { return nothing; } - const area = this._area(this.areaId, this._areas); + const area = this.hass.areas[this.areaId]; if (!area) { return html` @@ -182,8 +153,8 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) { const memberships = this._memberships( this.areaId, - this._devices, - this._entities + Object.values(this.hass.devices), + this._entityReg ); const { devices, entities } = memberships; @@ -617,7 +588,7 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) { } private _renderScript(name: string, entityState: ScriptEntity) { - const entry = this._entities.find( + const entry = this._entityReg.find( (e) => e.entity_id === entityState.entity_id ); let url = `/config/script/show/${entityState.entity_id}`; @@ -657,7 +628,7 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) { } private async _deleteConfirm() { - const area = this._area(this.areaId, this._areas); + const area = this.hass.areas[this.areaId]; showConfirmationDialog(this, { title: this.hass.localize( "ui.panel.config.areas.delete.confirmation_title", @@ -686,7 +657,6 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) { font-weight: 500; color: var(--secondary-text-color); } - img { border-radius: var(--ha-card-border-radius, 12px); width: 100%; diff --git a/src/panels/config/areas/ha-config-areas-dashboard.ts b/src/panels/config/areas/ha-config-areas-dashboard.ts index 0d294156b1..0dad5dc63c 100644 --- a/src/panels/config/areas/ha-config-areas-dashboard.ts +++ b/src/panels/config/areas/ha-config-areas-dashboard.ts @@ -1,7 +1,13 @@ import { mdiHelpCircle, mdiPlus } from "@mdi/js"; -import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { + CSSResultGroup, + LitElement, + TemplateResult, + css, + html, + nothing, +} from "lit"; +import { customElement, property } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; import { formatListWithAnds } from "../../../common/string/format-list"; @@ -11,19 +17,9 @@ import "../../../components/ha-svg-icon"; import { AreaRegistryEntry, createAreaRegistryEntry, - subscribeAreaRegistry, } from "../../../data/area_registry"; -import { - DeviceRegistryEntry, - subscribeDeviceRegistry, -} from "../../../data/device_registry"; -import { - EntityRegistryEntry, - subscribeEntityRegistry, -} from "../../../data/entity_registry"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-tabs-subpage"; -import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { HomeAssistant, Route } from "../../../types"; import "../ha-config-section"; import { configSections } from "../ha-panel-config"; @@ -33,7 +29,7 @@ import { } from "./show-dialog-area-registry-detail"; @customElement("ha-config-areas-dashboard") -export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { +export class HaConfigAreasDashboard extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ type: Boolean }) public isWide = false; @@ -42,24 +38,18 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { @property({ attribute: false }) public route!: Route; - @state() private _areas!: AreaRegistryEntry[]; - - @state() private _devices!: DeviceRegistryEntry[]; - - @state() private _entities!: EntityRegistryEntry[]; - private _processAreas = memoizeOne( ( - areas: AreaRegistryEntry[], - devices: DeviceRegistryEntry[], - entities: EntityRegistryEntry[] - ) => - areas.map((area) => { + areas: HomeAssistant["areas"], + devices: HomeAssistant["devices"], + entities: HomeAssistant["entities"] + ) => { + const processArea = (area: AreaRegistryEntry) => { let noDevicesInArea = 0; let noServicesInArea = 0; let noEntitiesInArea = 0; - for (const device of devices) { + for (const device of Object.values(devices)) { if (device.area_id === area.area_id) { if (device.entry_type === "service") { noServicesInArea++; @@ -69,7 +59,7 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { } } - for (const entity of entities) { + for (const entity of Object.values(entities)) { if (entity.area_id === area.area_id) { noEntitiesInArea++; } @@ -81,24 +71,22 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { services: noServicesInArea, entities: noEntitiesInArea, }; - }) + }; + + return Object.values(areas).map(processArea); + } ); - protected hassSubscribe(): (UnsubscribeFunc | Promise)[] { - return [ - subscribeAreaRegistry(this.hass.connection, (areas) => { - this._areas = areas; - }), - subscribeDeviceRegistry(this.hass.connection, (entries) => { - this._devices = entries; - }), - subscribeEntityRegistry(this.hass.connection, (entries) => { - this._entities = entries; - }), - ]; - } - protected render(): TemplateResult { + const areas = + !this.hass.areas || !this.hass.devices || !this.hass.entities + ? undefined + : this._processAreas( + this.hass.areas, + this.hass.devices, + this.hass.entities + ); + return html` + +
+ ${!area.picture && area.icon + ? html`` + : ""} +
+

${area.name}

+
+
+ ${formatListWithAnds( + this.hass.locale, + [ + area.devices && + this.hass.localize( + "ui.panel.config.integrations.config_entry.devices", + { count: area.devices } + ), + area.services && + this.hass.localize( + "ui.panel.config.integrations.config_entry.services", + { count: area.services } + ), + area.entities && + this.hass.localize( + "ui.panel.config.integrations.config_entry.entities", + { count: area.entities } + ), + ].filter((v): v is string => Boolean(v)) + )} +
+
+
+ `; + } + protected firstUpdated(changedProps) { super.firstUpdated(changedProps); loadAreaRegistryDetailDialog(); } private _createArea() { - this._openDialog(); + this._openAreaDialog(); } private _showHelp() { @@ -202,7 +191,7 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { }); } - private _openDialog(entry?: AreaRegistryEntry) { + private _openAreaDialog(entry?: AreaRegistryEntry) { showAreaRegistryDetailDialog(this, { entry, createEntry: async (values) => @@ -213,14 +202,17 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { static get styles(): CSSResultGroup { return css` .container { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - grid-gap: 16px 16px; padding: 8px 16px 16px; margin: 0 auto 64px auto; - max-width: 2000px; } - .container > * { + .areas { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + grid-gap: 16px 16px; + max-width: 2000px; + margin-bottom: 16px; + } + .areas > * { max-width: 500px; } ha-card { @@ -239,6 +231,12 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { background-position: center; position: relative; } + .placeholder { + display: flex; + align-items: center; + justify-content: center; + --mdc-icon-size: 48px; + } .picture.placeholder::before { position: absolute; content: ""; diff --git a/src/translations/en.json b/src/translations/en.json index 75cc2c4b02..5a887448f7 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1771,6 +1771,7 @@ "update_area": "Update area", "delete": "Delete", "name": "Name", + "icon": "Icon", "name_required": "Name is required", "area_id": "Area ID", "unknown_error": "Unknown error",