Add icon to areas (#19585)

* Add icon to areas

* Fix gallery

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
This commit is contained in:
Bram Kragten 2024-01-31 14:18:43 +01:00 committed by GitHub
parent b159f4c074
commit f4859320eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 176 additions and 168 deletions

View File

@ -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 { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
import "../../../../src/components/ha-form/ha-form"; import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/types"; 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 { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
@ -97,22 +98,25 @@ const DEVICES = [
}, },
]; ];
const AREAS = [ const AREAS: AreaRegistryEntry[] = [
{ {
area_id: "backyard", area_id: "backyard",
name: "Backyard", name: "Backyard",
icon: null,
picture: null, picture: null,
aliases: [], aliases: [],
}, },
{ {
area_id: "bedroom", area_id: "bedroom",
name: "Bedroom", name: "Bedroom",
icon: "mdi:bed",
picture: null, picture: null,
aliases: [], aliases: [],
}, },
{ {
area_id: "livingroom", area_id: "livingroom",
name: "Livingroom", name: "Livingroom",
icon: "mdi:sofa",
picture: null, picture: null,
aliases: [], aliases: [],
}, },

View File

@ -9,6 +9,7 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import "../../../../src/components/ha-selector/ha-selector"; import "../../../../src/components/ha-selector/ha-selector";
import "../../../../src/components/ha-settings-row"; import "../../../../src/components/ha-settings-row";
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
import { BlueprintInput } from "../../../../src/data/blueprint"; import { BlueprintInput } from "../../../../src/data/blueprint";
import { showDialog } from "../../../../src/dialogs/make-dialog-manager"; import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
@ -93,22 +94,25 @@ const DEVICES = [
}, },
]; ];
const AREAS = [ const AREAS: AreaRegistryEntry[] = [
{ {
area_id: "backyard", area_id: "backyard",
name: "Backyard", name: "Backyard",
icon: null,
picture: null, picture: null,
aliases: [], aliases: [],
}, },
{ {
area_id: "bedroom", area_id: "bedroom",
name: "Bedroom", name: "Bedroom",
icon: "mdi:bed",
picture: null, picture: null,
aliases: [], aliases: [],
}, },
{ {
area_id: "livingroom", area_id: "livingroom",
name: "Livingroom", name: "Livingroom",
icon: "mdi:sofa",
picture: null, picture: null,
aliases: [], aliases: [],
}, },

View File

@ -1,6 +1,6 @@
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { HassEntity } from "home-assistant-js-websocket"; 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 { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@ -36,8 +36,12 @@ type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry;
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (item) => const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (item) =>
html`<ha-list-item html`<ha-list-item
graphic="icon"
class=${classMap({ "add-new": item.area_id === "add_new" })} class=${classMap({ "add-new": item.area_id === "add_new" })}
> >
${item.icon
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
: nothing}
${item.name} ${item.name}
</ha-list-item>`; </ha-list-item>`;
@ -135,6 +139,7 @@ export class HaAreaPicker extends LitElement {
area_id: "no_areas", area_id: "no_areas",
name: this.hass.localize("ui.components.area-picker.no_areas"), name: this.hass.localize("ui.components.area-picker.no_areas"),
picture: null, picture: null,
icon: null,
aliases: [], aliases: [],
}, },
]; ];
@ -262,7 +267,9 @@ export class HaAreaPicker extends LitElement {
} }
if (areaIds) { if (areaIds) {
outputAreas = areas.filter((area) => areaIds!.includes(area.area_id)); outputAreas = outputAreas.filter((area) =>
areaIds!.includes(area.area_id)
);
} }
if (excludeAreas) { if (excludeAreas) {
@ -277,6 +284,7 @@ export class HaAreaPicker extends LitElement {
area_id: "no_areas", area_id: "no_areas",
name: this.hass.localize("ui.components.area-picker.no_match"), name: this.hass.localize("ui.components.area-picker.no_match"),
picture: null, picture: null,
icon: null,
aliases: [], aliases: [],
}, },
]; ];
@ -290,6 +298,7 @@ export class HaAreaPicker extends LitElement {
area_id: "add_new", area_id: "add_new",
name: this.hass.localize("ui.components.area-picker.add_new"), name: this.hass.localize("ui.components.area-picker.add_new"),
picture: null, picture: null,
icon: "mdi:plus",
aliases: [], aliases: [],
}, },
]; ];

View File

@ -98,6 +98,7 @@ export class HaTargetPicker extends LitElement {
area_id, area_id,
area?.name || area_id, area?.name || area_id,
undefined, undefined,
area?.icon,
mdiSofa mdiSofa
); );
}) })
@ -110,6 +111,7 @@ export class HaTargetPicker extends LitElement {
device_id, device_id,
device ? computeDeviceName(device, this.hass) : device_id, device ? computeDeviceName(device, this.hass) : device_id,
undefined, undefined,
undefined,
mdiDevices mdiDevices
); );
}) })
@ -209,7 +211,8 @@ export class HaTargetPicker extends LitElement {
id: string, id: string,
name: string, name: string,
entityState?: HassEntity, entityState?: HassEntity,
iconPath?: string icon?: string | null,
fallbackIconPath?: string
) { ) {
return html` return html`
<div <div
@ -217,12 +220,17 @@ export class HaTargetPicker extends LitElement {
[type]: true, [type]: true,
})}" })}"
> >
${iconPath ${icon
? html`<ha-svg-icon ? html`<ha-icon
class="mdc-chip__icon mdc-chip__icon--leading" class="mdc-chip__icon mdc-chip__icon--leading"
.path=${iconPath} .icon=${icon}
></ha-svg-icon>` ></ha-icon>`
: ""} : fallbackIconPath
? html`<ha-svg-icon
class="mdc-chip__icon mdc-chip__icon--leading"
.path=${fallbackIconPath}
></ha-svg-icon>`
: ""}
${entityState ${entityState
? html`<ha-state-icon ? html`<ha-state-icon
class="mdc-chip__icon mdc-chip__icon--leading" class="mdc-chip__icon mdc-chip__icon--leading"

View File

@ -9,6 +9,7 @@ export interface AreaRegistryEntry {
area_id: string; area_id: string;
name: string; name: string;
picture: string | null; picture: string | null;
icon: string | null;
aliases: string[]; aliases: string[];
} }
@ -23,6 +24,7 @@ export interface AreaDeviceLookup {
export interface AreaRegistryEntryMutableParams { export interface AreaRegistryEntryMutableParams {
name: string; name: string;
picture?: string | null; picture?: string | null;
icon?: string | null;
aliases?: string[]; aliases?: string[];
} }

View File

@ -1,19 +1,12 @@
import { Connection, createCollection } from "home-assistant-js-websocket"; import { Connection, createCollection } from "home-assistant-js-websocket";
import { Store } from "home-assistant-js-websocket/dist/store"; import { Store } from "home-assistant-js-websocket/dist/store";
import { stringCompare } from "../common/string/compare";
import { AreaRegistryEntry } from "./area_registry";
import { debounce } from "../common/util/debounce"; import { debounce } from "../common/util/debounce";
import { AreaRegistryEntry } from "./area_registry";
const fetchAreaRegistry = (conn: Connection) => const fetchAreaRegistry = (conn: Connection) =>
conn conn.sendMessagePromise<AreaRegistryEntry[]>({
.sendMessagePromise({ type: "config/area_registry/list",
type: "config/area_registry/list", });
})
.then((areas) =>
(areas as AreaRegistryEntry[]).sort((ent1, ent2) =>
stringCompare(ent1.name, ent2.name)
)
);
const subscribeAreaRegistryUpdates = ( const subscribeAreaRegistryUpdates = (
conn: Connection, conn: Connection,

View File

@ -9,6 +9,7 @@ import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-picture-upload"; import "../../../components/ha-picture-upload";
import type { HaPictureUpload } from "../../../components/ha-picture-upload"; import type { HaPictureUpload } from "../../../components/ha-picture-upload";
import "../../../components/ha-settings-row"; import "../../../components/ha-settings-row";
import "../../../components/ha-icon-picker";
import "../../../components/ha-textfield"; import "../../../components/ha-textfield";
import { AreaRegistryEntryMutableParams } from "../../../data/area_registry"; import { AreaRegistryEntryMutableParams } from "../../../data/area_registry";
import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog"; 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 _picture!: string | null;
@state() private _icon!: string | null;
@state() private _error?: string; @state() private _error?: string;
@state() private _params?: AreaRegistryDetailDialogParams; @state() private _params?: AreaRegistryDetailDialogParams;
@ -46,6 +49,7 @@ class DialogAreaDetail extends LitElement {
this._name = this._params.entry ? this._params.entry.name : ""; this._name = this._params.entry ? this._params.entry.name : "";
this._aliases = this._params.entry ? this._params.entry.aliases : []; this._aliases = this._params.entry ? this._params.entry.aliases : [];
this._picture = this._params.entry?.picture || null; this._picture = this._params.entry?.picture || null;
this._icon = this._params.entry?.icon || null;
await this.updateComplete; await this.updateComplete;
} }
@ -101,6 +105,13 @@ class DialogAreaDetail extends LitElement {
dialogInitialFocus dialogInitialFocus
></ha-textfield> ></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
.value=${this._icon}
@value-changed=${this._iconChanged}
.label=${this.hass.localize("ui.panel.config.areas.editor.icon")}
></ha-icon-picker>
<ha-picture-upload <ha-picture-upload
.hass=${this.hass} .hass=${this.hass}
.value=${this._picture} .value=${this._picture}
@ -152,23 +163,30 @@ class DialogAreaDetail extends LitElement {
this._name = ev.target.value; this._name = ev.target.value;
} }
private _iconChanged(ev) {
this._error = undefined;
this._icon = ev.detail.value;
}
private _pictureChanged(ev: ValueChangedEvent<string | null>) { private _pictureChanged(ev: ValueChangedEvent<string | null>) {
this._error = undefined; this._error = undefined;
this._picture = (ev.target as HaPictureUpload).value; this._picture = (ev.target as HaPictureUpload).value;
} }
private async _updateEntry() { private async _updateEntry() {
const create = !this._params!.entry;
this._submitting = true; this._submitting = true;
try { try {
const values: AreaRegistryEntryMutableParams = { const values: AreaRegistryEntryMutableParams = {
name: this._name.trim(), name: this._name.trim(),
picture: this._picture, picture: this._picture || (create ? undefined : null),
icon: this._icon || (create ? undefined : null),
aliases: this._aliases, aliases: this._aliases,
}; };
if (this._params!.entry) { if (create) {
await this._params!.updateEntry!(values);
} else {
await this._params!.createEntry!(values); await this._params!.createEntry!(values);
} else {
await this._params!.updateEntry!(values);
} }
this.closeDialog(); this.closeDialog();
} catch (err: any) { } catch (err: any) {
@ -189,6 +207,7 @@ class DialogAreaDetail extends LitElement {
haStyleDialog, haStyleDialog,
css` css`
ha-textfield, ha-textfield,
ha-icon-picker,
ha-picture-upload { ha-picture-upload {
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;

View File

@ -1,11 +1,9 @@
import { consume } from "@lit-labs/context";
import "@material/mwc-button"; import "@material/mwc-button";
import "@material/mwc-list"; import "@material/mwc-list";
import { mdiDelete, mdiDotsVertical, mdiImagePlus, mdiPencil } from "@mdi/js"; import { mdiDelete, mdiDotsVertical, mdiImagePlus, mdiPencil } from "@mdi/js";
import { import { HassEntity } from "home-assistant-js-websocket/dist/types";
HassEntity, import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
UnsubscribeFunc,
} from "home-assistant-js-websocket/dist/types";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@ -18,33 +16,31 @@ import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next"; import "../../../components/ha-icon-next";
import "../../../components/ha-list-item";
import { import {
AreaRegistryEntry, AreaRegistryEntry,
deleteAreaRegistryEntry, deleteAreaRegistryEntry,
subscribeAreaRegistry,
updateAreaRegistryEntry, updateAreaRegistryEntry,
} from "../../../data/area_registry"; } from "../../../data/area_registry";
import { AutomationEntity } from "../../../data/automation"; import { AutomationEntity } from "../../../data/automation";
import { fullEntitiesContext } from "../../../data/context";
import { import {
computeDeviceName,
DeviceRegistryEntry, DeviceRegistryEntry,
computeDeviceName,
sortDeviceRegistryByName, sortDeviceRegistryByName,
subscribeDeviceRegistry,
} from "../../../data/device_registry"; } from "../../../data/device_registry";
import { import {
computeEntityRegistryName,
EntityRegistryEntry, EntityRegistryEntry,
computeEntityRegistryName,
sortEntityRegistryByName, sortEntityRegistryByName,
subscribeEntityRegistry,
} from "../../../data/entity_registry"; } from "../../../data/entity_registry";
import { SceneEntity } from "../../../data/scene"; import { SceneEntity } from "../../../data/scene";
import { ScriptEntity } from "../../../data/script"; 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 { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog"; import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import "../../../layouts/hass-error-screen"; import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-subpage"; import "../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import "../../logbook/ha-logbook"; import "../../logbook/ha-logbook";
@ -52,7 +48,6 @@ import {
loadAreaRegistryDetailDialog, loadAreaRegistryDetailDialog,
showAreaRegistryDetailDialog, showAreaRegistryDetailDialog,
} from "./show-dialog-area-registry-detail"; } from "./show-dialog-area-registry-detail";
import "../../../components/ha-list-item";
declare type NameAndEntity<EntityType extends HassEntity> = { declare type NameAndEntity<EntityType extends HassEntity> = {
name: string; name: string;
@ -60,7 +55,7 @@ declare type NameAndEntity<EntityType extends HassEntity> = {
}; };
@customElement("ha-config-area-page") @customElement("ha-config-area-page")
class HaConfigAreaPage extends SubscribeMixin(LitElement) { class HaConfigAreaPage extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public areaId!: string; @property() public areaId!: string;
@ -71,24 +66,14 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public showAdvanced = false; @property({ type: Boolean }) public showAdvanced = false;
@state() public _areas!: AreaRegistryEntry[]; @state()
@consume({ context: fullEntitiesContext, subscribe: true })
@state() public _devices!: DeviceRegistryEntry[]; _entityReg!: EntityRegistryEntry[];
@state() public _entities!: EntityRegistryEntry[];
@state() private _related?: RelatedResult; @state() private _related?: RelatedResult;
private _logbookTime = { recent: 86400 }; private _logbookTime = { recent: 86400 };
private _area = memoizeOne(
(
areaId: string,
areas: AreaRegistryEntry[]
): AreaRegistryEntry | undefined =>
areas.find((area) => area.area_id === areaId)
);
private _memberships = memoizeOne( private _memberships = memoizeOne(
( (
areaId: string, areaId: string,
@ -150,26 +135,12 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
} }
} }
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
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() { protected render() {
if (!this._areas || !this._devices || !this._entities) { if (!this.hass.areas || !this.hass.devices || !this.hass.entities) {
return nothing; return nothing;
} }
const area = this._area(this.areaId, this._areas); const area = this.hass.areas[this.areaId];
if (!area) { if (!area) {
return html` return html`
@ -182,8 +153,8 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
const memberships = this._memberships( const memberships = this._memberships(
this.areaId, this.areaId,
this._devices, Object.values(this.hass.devices),
this._entities this._entityReg
); );
const { devices, entities } = memberships; const { devices, entities } = memberships;
@ -617,7 +588,7 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
} }
private _renderScript(name: string, entityState: ScriptEntity) { private _renderScript(name: string, entityState: ScriptEntity) {
const entry = this._entities.find( const entry = this._entityReg.find(
(e) => e.entity_id === entityState.entity_id (e) => e.entity_id === entityState.entity_id
); );
let url = `/config/script/show/${entityState.entity_id}`; let url = `/config/script/show/${entityState.entity_id}`;
@ -657,7 +628,7 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
} }
private async _deleteConfirm() { private async _deleteConfirm() {
const area = this._area(this.areaId, this._areas); const area = this.hass.areas[this.areaId];
showConfirmationDialog(this, { showConfirmationDialog(this, {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.areas.delete.confirmation_title", "ui.panel.config.areas.delete.confirmation_title",
@ -686,7 +657,6 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
font-weight: 500; font-weight: 500;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
img { img {
border-radius: var(--ha-card-border-radius, 12px); border-radius: var(--ha-card-border-radius, 12px);
width: 100%; width: 100%;

View File

@ -1,7 +1,13 @@
import { mdiHelpCircle, mdiPlus } from "@mdi/js"; import { mdiHelpCircle, mdiPlus } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import {
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit"; CSSResultGroup,
import { customElement, property, state } from "lit/decorators"; LitElement,
TemplateResult,
css,
html,
nothing,
} from "lit";
import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { formatListWithAnds } from "../../../common/string/format-list"; import { formatListWithAnds } from "../../../common/string/format-list";
@ -11,19 +17,9 @@ import "../../../components/ha-svg-icon";
import { import {
AreaRegistryEntry, AreaRegistryEntry,
createAreaRegistryEntry, createAreaRegistryEntry,
subscribeAreaRegistry,
} from "../../../data/area_registry"; } 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 { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-tabs-subpage"; import "../../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
import "../ha-config-section"; import "../ha-config-section";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
@ -33,7 +29,7 @@ import {
} from "./show-dialog-area-registry-detail"; } from "./show-dialog-area-registry-detail";
@customElement("ha-config-areas-dashboard") @customElement("ha-config-areas-dashboard")
export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { export class HaConfigAreasDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide = false; @property({ type: Boolean }) public isWide = false;
@ -42,24 +38,18 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public route!: Route; @property({ attribute: false }) public route!: Route;
@state() private _areas!: AreaRegistryEntry[];
@state() private _devices!: DeviceRegistryEntry[];
@state() private _entities!: EntityRegistryEntry[];
private _processAreas = memoizeOne( private _processAreas = memoizeOne(
( (
areas: AreaRegistryEntry[], areas: HomeAssistant["areas"],
devices: DeviceRegistryEntry[], devices: HomeAssistant["devices"],
entities: EntityRegistryEntry[] entities: HomeAssistant["entities"]
) => ) => {
areas.map((area) => { const processArea = (area: AreaRegistryEntry) => {
let noDevicesInArea = 0; let noDevicesInArea = 0;
let noServicesInArea = 0; let noServicesInArea = 0;
let noEntitiesInArea = 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.area_id === area.area_id) {
if (device.entry_type === "service") { if (device.entry_type === "service") {
noServicesInArea++; 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) { if (entity.area_id === area.area_id) {
noEntitiesInArea++; noEntitiesInArea++;
} }
@ -81,24 +71,22 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
services: noServicesInArea, services: noServicesInArea,
entities: noEntitiesInArea, entities: noEntitiesInArea,
}; };
}) };
return Object.values(areas).map(processArea);
}
); );
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
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 { 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` return html`
<hass-tabs-subpage <hass-tabs-subpage
.hass=${this.hass} .hass=${this.hass}
@ -115,52 +103,11 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
@click=${this._showHelp} @click=${this._showHelp}
></ha-icon-button> ></ha-icon-button>
<div class="container"> <div class="container">
${!this._areas || !this._devices || !this._entities ${areas?.length
? "" ? html`<div class="areas">
: this._processAreas( ${areas.map((area) => this._renderArea(area))}
this._areas, </div>`
this._devices, : nothing}
this._entities
).map(
(area) =>
html`<a href=${`/config/areas/area/${area.area_id}`}
><ha-card outlined>
<div
style=${styleMap({
backgroundImage: area.picture
? `url(${area.picture})`
: undefined,
})}
class="picture ${!area.picture ? "placeholder" : ""}"
></div>
<h1 class="card-header">${area.name}</h1>
<div class="card-content">
<div>
${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))
)}
</div>
</div>
</ha-card></a
>`
)}
</div> </div>
<ha-fab <ha-fab
slot="fab" slot="fab"
@ -176,13 +123,55 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
`; `;
} }
private _renderArea(area) {
return html`<a href=${`/config/areas/area/${area.area_id}`}>
<ha-card outlined>
<div
style=${styleMap({
backgroundImage: area.picture ? `url(${area.picture})` : undefined,
})}
class="picture ${!area.picture ? "placeholder" : ""}"
>
${!area.picture && area.icon
? html`<ha-icon .icon=${area.icon}></ha-icon>`
: ""}
</div>
<h1 class="card-header">${area.name}</h1>
<div class="card-content">
<div>
${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))
)}
</div>
</div>
</ha-card>
</a>`;
}
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
loadAreaRegistryDetailDialog(); loadAreaRegistryDetailDialog();
} }
private _createArea() { private _createArea() {
this._openDialog(); this._openAreaDialog();
} }
private _showHelp() { private _showHelp() {
@ -202,7 +191,7 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
}); });
} }
private _openDialog(entry?: AreaRegistryEntry) { private _openAreaDialog(entry?: AreaRegistryEntry) {
showAreaRegistryDetailDialog(this, { showAreaRegistryDetailDialog(this, {
entry, entry,
createEntry: async (values) => createEntry: async (values) =>
@ -213,14 +202,17 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
.container { .container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: 16px 16px;
padding: 8px 16px 16px; padding: 8px 16px 16px;
margin: 0 auto 64px auto; 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; max-width: 500px;
} }
ha-card { ha-card {
@ -239,6 +231,12 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
background-position: center; background-position: center;
position: relative; position: relative;
} }
.placeholder {
display: flex;
align-items: center;
justify-content: center;
--mdc-icon-size: 48px;
}
.picture.placeholder::before { .picture.placeholder::before {
position: absolute; position: absolute;
content: ""; content: "";

View File

@ -1771,6 +1771,7 @@
"update_area": "Update area", "update_area": "Update area",
"delete": "Delete", "delete": "Delete",
"name": "Name", "name": "Name",
"icon": "Icon",
"name_required": "Name is required", "name_required": "Name is required",
"area_id": "Area ID", "area_id": "Area ID",
"unknown_error": "Unknown error", "unknown_error": "Unknown error",