From 2c0c48106de46e60b33f707939e55e64c21b0b6e Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 19 Mar 2025 21:11:46 +0100 Subject: [PATCH] Add entities filtering and reordering for areas strategy dashboard (#24677) * Add entities editor * Save entities per domain and area * Use hidden and reorder logic in dashboard * Add overview hidden logic * Don't use icon for nav * Remove overview hidden * Change default text * Fix icons * Rename config properties --- src/common/string/compare.ts | 19 +++ src/components/ha-areas-display-editor.ts | 6 +- src/components/ha-entities-display-editor.ts | 81 +++++++++ src/components/ha-items-display-editor.ts | 74 ++++++--- .../{area => areas}/area-view-strategy.ts | 157 ++++-------------- .../areas/areas-dashboard-strategy.ts | 15 +- .../strategies/areas/areas-view-strategy.ts | 18 +- .../hui-areas-dashboard-strategy-editor.ts | 119 ++++++++++++- .../areas/helpers/area-strategy-helper.ts | 149 +++++++++++++++++ .../lovelace/strategies/get-strategy.ts | 2 +- 10 files changed, 489 insertions(+), 151 deletions(-) create mode 100644 src/components/ha-entities-display-editor.ts rename src/panels/lovelace/strategies/{area => areas}/area-view-strategy.ts (55%) create mode 100644 src/panels/lovelace/strategies/areas/helpers/area-strategy-helper.ts diff --git a/src/common/string/compare.ts b/src/common/string/compare.ts index a14e087ccb..65952c2922 100644 --- a/src/common/string/compare.ts +++ b/src/common/string/compare.ts @@ -45,3 +45,22 @@ export const caseInsensitiveStringCompare = ( return fallbackStringCompare(a.toLowerCase(), b.toLowerCase()); }; + +export const orderCompare = (order: string[]) => (a: string, b: string) => { + const idxA = order.indexOf(a); + const idxB = order.indexOf(b); + + if (idxA === idxB) { + return 0; + } + + if (idxA === -1) { + return 1; + } + + if (idxB === -1) { + return -1; + } + + return idxA - idxB; +}; diff --git a/src/components/ha-areas-display-editor.ts b/src/components/ha-areas-display-editor.ts index 340ae2126f..4215b08c0c 100644 --- a/src/components/ha-areas-display-editor.ts +++ b/src/components/ha-areas-display-editor.ts @@ -33,8 +33,11 @@ export class HaAreasDisplayEditor extends LitElement { @property({ type: Boolean }) public required = false; + @property({ type: Boolean, attribute: "show-navigation-button" }) + public showNavigationButton = false; + protected render(): TemplateResult { - const compare = areaCompare(this.hass.areas, this.value?.order); + const compare = areaCompare(this.hass.areas); const areas = Object.values(this.hass.areas).sort((areaA, areaB) => compare(areaA.area_id, areaB.area_id) @@ -68,6 +71,7 @@ export class HaAreasDisplayEditor extends LitElement { .items=${items} .value=${value} @value-changed=${this._areaDisplayChanged} + .showNavigationButton=${this.showNavigationButton} > `; diff --git a/src/components/ha-entities-display-editor.ts b/src/components/ha-entities-display-editor.ts new file mode 100644 index 0000000000..8f80dea2be --- /dev/null +++ b/src/components/ha-entities-display-editor.ts @@ -0,0 +1,81 @@ +import type { TemplateResult } from "lit"; +import { LitElement, html } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../common/dom/fire_event"; +import { computeStateName } from "../common/entity/compute_state_name"; +import { entityIcon } from "../data/icons"; +import type { HomeAssistant } from "../types"; +import "./ha-items-display-editor"; +import type { DisplayItem, DisplayValue } from "./ha-items-display-editor"; + +export interface EntitiesDisplayValue { + hidden?: string[]; + order?: string[]; +} + +@customElement("ha-entities-display-editor") +export class HaEntitiesDisplayEditor extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public label?: string; + + @property({ attribute: false }) public value?: EntitiesDisplayValue; + + @property({ attribute: false }) public entitiesIds: string[] = []; + + @property() public helper?: string; + + @property({ type: Boolean }) public expanded = false; + + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public required = false; + + protected render(): TemplateResult { + const entities = this.entitiesIds + .map((entityId) => this.hass.states[entityId]) + .filter(Boolean); + + const items: DisplayItem[] = entities.map((entity) => ({ + value: entity.entity_id, + label: computeStateName(entity), + icon: entityIcon(this.hass, entity), + })); + + const value: DisplayValue = { + order: this.value?.order ?? [], + hidden: this.value?.hidden ?? [], + }; + + return html` + + `; + } + + private _itemDisplayChanged(ev) { + ev.stopPropagation(); + const value = ev.detail.value as DisplayValue; + const newValue: EntitiesDisplayValue = { + ...this.value, + ...value, + }; + if (newValue.hidden?.length === 0) { + delete newValue.hidden; + } + if (newValue.order?.length === 0) { + delete newValue.order; + } + fireEvent(this, "value-changed", { value: newValue }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-entities-display-editor": HaEntitiesDisplayEditor; + } +} diff --git a/src/components/ha-items-display-editor.ts b/src/components/ha-items-display-editor.ts index 6feb75c207..af7fb2ba1f 100644 --- a/src/components/ha-items-display-editor.ts +++ b/src/components/ha-items-display-editor.ts @@ -1,22 +1,26 @@ import { ResizeController } from "@lit-labs/observers/resize-controller"; import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js"; +import type { TemplateResult } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; +import { ifDefined } from "lit/directives/if-defined"; import { repeat } from "lit/directives/repeat"; +import { until } from "lit/directives/until"; import memoizeOne from "memoize-one"; import { fireEvent } from "../common/dom/fire_event"; +import { orderCompare } from "../common/string/compare"; import type { HomeAssistant } from "../types"; import "./ha-icon"; import "./ha-icon-button"; -import "./ha-icon-button-next"; +import "./ha-icon-next"; import "./ha-md-list"; import "./ha-md-list-item"; import "./ha-sortable"; import "./ha-svg-icon"; export interface DisplayItem { - icon?: string; + icon?: string | Promise; iconPath?: string; value: string; label: string; @@ -52,6 +56,10 @@ export class HaItemDisplayEditor extends LitElement { hidden: [], }; + @property({ attribute: false }) public actionsRenderer?: ( + item: DisplayItem + ) => TemplateResult<1> | typeof nothing; + private _showIcon = new ResizeController(this, { callback: (entries) => entries[0]?.contentRect.width > 450, }); @@ -70,7 +78,11 @@ export class HaItemDisplayEditor extends LitElement { newHidden.push(value); } - const newVisibleItems = this._visibleItems(this.items, newHidden); + const newVisibleItems = this._visibleItems( + this.items, + newHidden, + this.value.order + ); const newOrder = newVisibleItems.map((a) => a.value); this.value = { @@ -84,7 +96,11 @@ export class HaItemDisplayEditor extends LitElement { ev.stopPropagation(); const { oldIndex, newIndex } = ev.detail; - const visibleItems = this._visibleItems(this.items, this.value.hidden); + const visibleItems = this._visibleItems( + this.items, + this.value.hidden, + this.value.order + ); const newOrder = visibleItems.map((item) => item.value); const movedItem = newOrder.splice(oldIndex, 1)[0]; @@ -103,8 +119,21 @@ export class HaItemDisplayEditor extends LitElement { ev.stopPropagation(); } - private _visibleItems = memoizeOne((items: DisplayItem[], hidden: string[]) => - items.filter((item) => !hidden.includes(item.value)) + private _visibleItems = memoizeOne( + (items: DisplayItem[], hidden: string[], order: string[]) => { + const compare = orderCompare(order); + return items + .filter((item) => !hidden.includes(item.value)) + .sort((a, b) => compare(a.value, b.value)); + } + ); + + private _allItems = memoizeOne( + (items: DisplayItem[], hidden: string[], order: string[]) => { + const visibleItems = this._visibleItems(items, hidden, order); + const hiddenItems = this._hiddenItems(items, hidden); + return [...visibleItems, ...hiddenItems]; + } ); private _hiddenItems = memoizeOne((items: DisplayItem[], hidden: string[]) => @@ -112,10 +141,11 @@ export class HaItemDisplayEditor extends LitElement { ); protected render() { - const allItems = [ - ...this._visibleItems(this.items, this.value.hidden), - ...this._hiddenItems(this.items, this.value.hidden), - ]; + const allItems = this._allItems( + this.items, + this.value.hidden, + this.value.order + ); const showIcon = this._showIcon.value; return html` @@ -128,11 +158,18 @@ export class HaItemDisplayEditor extends LitElement { ${repeat( allItems, (item) => item.value, - (item, _idx) => { + (item: DisplayItem, _idx) => { const isVisible = !this.value.hidden.includes(item.value); const { label, value, description, icon, iconPath } = item; return html` ` @@ -170,6 +207,11 @@ export class HaItemDisplayEditor extends LitElement { > ` : nothing} + ${this.actionsRenderer + ? html` + ${this.actionsRenderer(item)} + ` + : nothing} ${this.showNavigationButton - ? html` - - ` + ? html` ` : nothing} `; diff --git a/src/panels/lovelace/strategies/area/area-view-strategy.ts b/src/panels/lovelace/strategies/areas/area-view-strategy.ts similarity index 55% rename from src/panels/lovelace/strategies/area/area-view-strategy.ts rename to src/panels/lovelace/strategies/areas/area-view-strategy.ts index 03bd7d6dfc..1fdc1d2f0e 100644 --- a/src/panels/lovelace/strategies/area/area-view-strategy.ts +++ b/src/panels/lovelace/strategies/areas/area-view-strategy.ts @@ -1,7 +1,5 @@ import { ReactiveElement } from "lit"; import { customElement } from "lit/decorators"; -import type { EntityFilterFunc } from "../../../../common/entity/entity_filter"; -import { generateEntityFilter } from "../../../../common/entity/entity_filter"; import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge"; import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card"; import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section"; @@ -13,118 +11,21 @@ import { supportsLightBrightnessCardFeature } from "../../card-features/hui-ligh import { supportsLockCommandsCardFeature } from "../../card-features/hui-lock-commands-card-feature"; import { supportsTargetTemperatureCardFeature } from "../../card-features/hui-target-temperature-card-feature"; import type { LovelaceCardFeatureConfig } from "../../card-features/types"; +import { + AREA_STRATEGY_GROUP_ICONS, + AREA_STRATEGY_GROUP_LABELS, + getAreaGroupedEntities, +} from "./helpers/area-strategy-helper"; -type Group = "lights" | "climate" | "media_players" | "security"; - -type AreaEntitiesByGroup = Record; - -type AreaFilteredByGroup = Record; - -export const getAreaGroupedEntities = ( - area: string, - hass: HomeAssistant, - controlOnly = false -): AreaEntitiesByGroup => { - const allEntities = Object.keys(hass.states); - - const groupedFilters: AreaFilteredByGroup = { - lights: [ - generateEntityFilter(hass, { - domain: "light", - area: area, - entity_category: "none", - }), - ], - climate: [ - generateEntityFilter(hass, { - domain: "climate", - area: area, - entity_category: "none", - }), - generateEntityFilter(hass, { - domain: "humidifier", - area: area, - entity_category: "none", - }), - generateEntityFilter(hass, { - domain: "cover", - area: area, - device_class: [ - "shutter", - "awning", - "blind", - "curtain", - "shade", - "shutter", - "window", - ], - entity_category: "none", - }), - ...(controlOnly - ? [] - : [ - generateEntityFilter(hass, { - domain: "binary_sensor", - area: area, - device_class: "window", - entity_category: "none", - }), - ]), - ], - media_players: [ - generateEntityFilter(hass, { - domain: "media_player", - area: area, - entity_category: "none", - }), - ], - security: [ - generateEntityFilter(hass, { - domain: "alarm_control_panel", - area: area, - entity_category: "none", - }), - generateEntityFilter(hass, { - domain: "lock", - area: area, - entity_category: "none", - }), - generateEntityFilter(hass, { - domain: "cover", - device_class: ["door", "garage", "gate"], - area: area, - entity_category: "none", - }), - ...(controlOnly - ? [] - : [ - generateEntityFilter(hass, { - domain: "binary_sensor", - device_class: ["door", "garage_door"], - area: area, - entity_category: "none", - }), - ]), - ], - }; - - return Object.fromEntries( - Object.entries(groupedFilters).map(([group, filters]) => [ - group, - filters.reduce( - (acc, filter) => [ - ...acc, - ...allEntities.filter((entity) => filter(entity)), - ], - [] - ), - ]) - ) as AreaEntitiesByGroup; -}; +export interface EntitiesDisplay { + hidden?: string[]; + order?: string[]; +} export interface AreaViewStrategyConfig { type: "area"; area?: string; + groups_options?: Record; } const computeTileCardConfig = @@ -207,21 +108,24 @@ export class AreaViewStrategy extends ReactiveElement { }); } - const groupedEntities = getAreaGroupedEntities(config.area, hass); + const groupedEntities = getAreaGroupedEntities( + config.area, + hass, + config.groups_options + ); const computeTileCard = computeTileCardConfig(hass); - const { - lights, - climate, - media_players: mediaPlayers, - security, - } = groupedEntities; + const { lights, climate, media_players, security } = groupedEntities; + if (lights.length > 0) { sections.push({ type: "grid", cards: [ - computeHeadingCard("Lights", "mdi:lightbulb"), + computeHeadingCard( + AREA_STRATEGY_GROUP_LABELS.lights, + AREA_STRATEGY_GROUP_ICONS.lights + ), ...lights.map(computeTileCard), ], }); @@ -231,18 +135,24 @@ export class AreaViewStrategy extends ReactiveElement { sections.push({ type: "grid", cards: [ - computeHeadingCard("Climate", "mdi:home-thermometer"), + computeHeadingCard( + AREA_STRATEGY_GROUP_LABELS.climate, + AREA_STRATEGY_GROUP_ICONS.climate + ), ...climate.map(computeTileCard), ], }); } - if (mediaPlayers.length > 0) { + if (media_players.length > 0) { sections.push({ type: "grid", cards: [ - computeHeadingCard("Entertainment", "mdi:multimedia"), - ...mediaPlayers.map(computeTileCard), + computeHeadingCard( + AREA_STRATEGY_GROUP_LABELS.media_players, + AREA_STRATEGY_GROUP_ICONS.media_players + ), + ...media_players.map(computeTileCard), ], }); } @@ -251,7 +161,10 @@ export class AreaViewStrategy extends ReactiveElement { sections.push({ type: "grid", cards: [ - computeHeadingCard("Security", "mdi:security"), + computeHeadingCard( + AREA_STRATEGY_GROUP_LABELS.security, + AREA_STRATEGY_GROUP_ICONS.security + ), ...security.map(computeTileCard), ], }); diff --git a/src/panels/lovelace/strategies/areas/areas-dashboard-strategy.ts b/src/panels/lovelace/strategies/areas/areas-dashboard-strategy.ts index 4773488762..6c6f96ae13 100644 --- a/src/panels/lovelace/strategies/areas/areas-dashboard-strategy.ts +++ b/src/panels/lovelace/strategies/areas/areas-dashboard-strategy.ts @@ -3,17 +3,25 @@ import { customElement } from "lit/decorators"; import type { LovelaceConfig } from "../../../../data/lovelace/config/types"; import type { LovelaceViewRawConfig } from "../../../../data/lovelace/config/view"; import type { HomeAssistant } from "../../../../types"; -import type { AreaViewStrategyConfig } from "../area/area-view-strategy"; +import type { + AreaViewStrategyConfig, + EntitiesDisplay, +} from "./area-view-strategy"; import type { LovelaceStrategyEditor } from "../types"; import type { AreasViewStrategyConfig } from "./areas-view-strategy"; import { computeAreaPath, getAreas } from "./helpers/areas-strategy-helpers"; +interface AreaOptions { + groups_options?: Record; +} + export interface AreasDashboardStrategyConfig { type: "areas"; areas_display?: { hidden?: string[]; order?: string[]; }; + areas_options?: Record; } @customElement("areas-dashboard-strategy") @@ -30,13 +38,15 @@ export class AreasDashboardStrategy extends ReactiveElement { const areaViews = areas.map((area) => { const path = computeAreaPath(area.area_id); + const areaConfig = config.areas_options?.[area.area_id]; + return { title: area.name, - icon: area.icon || undefined, path: path, strategy: { type: "area", area: area.area_id, + groups_options: areaConfig?.groups_options, } satisfies AreaViewStrategyConfig, }; }); @@ -50,6 +60,7 @@ export class AreasDashboardStrategy extends ReactiveElement { strategy: { type: "areas", areas_display: config.areas_display, + areas_options: config.areas_options, } satisfies AreasViewStrategyConfig, }, ...areaViews, diff --git a/src/panels/lovelace/strategies/areas/areas-view-strategy.ts b/src/panels/lovelace/strategies/areas/areas-view-strategy.ts index c6069f9b1a..00a6b81bf8 100644 --- a/src/panels/lovelace/strategies/areas/areas-view-strategy.ts +++ b/src/panels/lovelace/strategies/areas/areas-view-strategy.ts @@ -3,8 +3,13 @@ import { customElement } from "lit/decorators"; import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section"; import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view"; import type { HomeAssistant } from "../../../../types"; -import { getAreaGroupedEntities } from "../area/area-view-strategy"; +import { getAreaGroupedEntities } from "./helpers/area-strategy-helper"; import { computeAreaPath, getAreas } from "./helpers/areas-strategy-helpers"; +import type { EntitiesDisplay } from "./area-view-strategy"; + +interface AreaOptions { + groups_options?: Record; +} export interface AreasViewStrategyConfig { type: "areas"; @@ -12,6 +17,7 @@ export interface AreasViewStrategyConfig { hidden?: string[]; order?: string[]; }; + areas_options?: Record; } @customElement("areas-view-strategy") @@ -30,7 +36,13 @@ export class AreasViewStrategy extends ReactiveElement { .map((area) => { const path = computeAreaPath(area.area_id); - const groups = getAreaGroupedEntities(area.area_id, hass, true); + const areaConfig = config.areas_options?.[area.area_id]; + + const groups = getAreaGroupedEntities( + area.area_id, + hass, + areaConfig?.groups_options + ); const entities = [ ...groups.lights, @@ -67,7 +79,7 @@ export class AreasViewStrategy extends ReactiveElement { : [ { type: "markdown", - content: "No controllable devices in this area.", + content: "No entities in this area.", }, ]), ], diff --git a/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts b/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts index c1a9586b50..758d26902b 100644 --- a/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts +++ b/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts @@ -1,9 +1,20 @@ -import { html, LitElement, nothing } from "lit"; +import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; import "../../../../../components/ha-areas-display-editor"; import type { AreasDisplayValue } from "../../../../../components/ha-areas-display-editor"; +import "../../../../../components/ha-entities-display-editor"; +import "../../../../../components/ha-icon-button"; +import "../../../../../components/ha-icon-button-prev"; +import "../../../../../components/ha-icon"; import type { HomeAssistant } from "../../../../../types"; +import type { AreaStrategyGroup } from "../helpers/area-strategy-helper"; +import { + AREA_STRATEGY_GROUP_ICONS, + AREA_STRATEGY_GROUPS, + AREA_STRATEGY_GROUP_LABELS, + getAreaGroupedEntities, +} from "../helpers/area-strategy-helper"; import type { LovelaceStrategyEditor } from "../../types"; import type { AreasDashboardStrategyConfig } from "../areas-dashboard-strategy"; @@ -21,11 +32,62 @@ export class HuiAreasDashboardStrategyEditor this._config = config; } + @state() + private _area?: string; + protected render() { if (!this.hass || !this._config) { return nothing; } + if (this._area) { + const groups = getAreaGroupedEntities(this._area, this.hass); + + const area = this.hass.areas[this._area]; + + return html` +
+ +

${area.name}

+
+ ${AREA_STRATEGY_GROUPS.map((group) => { + const entities = groups[group] || []; + const value = + this._config!.areas_options?.[this._area!]?.groups_options?.[group]; + + return html` + + + ${entities.length + ? html` + + ` + : html` +

+ No entities in this section, it will not be displayed. +

+ `} +
+ `; + })} + `; + } + const value = this._config.areas_display; return html` @@ -35,13 +97,25 @@ export class HuiAreasDashboardStrategyEditor .label=${this.hass.localize( "ui.panel.lovelace.editor.strategy.areas.areas_display" )} - @value-changed=${this._areaDisplayChanged} + @value-changed=${this._areasDisplayChanged} expanded + show-navigation-button + @item-display-navigate-clicked=${this._handleAreaNavigate} > `; } - private _areaDisplayChanged(ev: CustomEvent): void { + private _back(): void { + if (this._area) { + this._area = undefined; + } + } + + private _handleAreaNavigate(ev: CustomEvent): void { + this._area = ev.detail.value; + } + + private _areasDisplayChanged(ev: CustomEvent): void { const value = ev.detail.value as AreasDisplayValue; const newConfig: AreasDashboardStrategyConfig = { ...this._config!, @@ -50,6 +124,45 @@ export class HuiAreasDashboardStrategyEditor fireEvent(this, "config-changed", { config: newConfig }); } + + private _entitiesDisplayChanged(ev: CustomEvent): void { + const value = ev.detail.value as AreasDisplayValue; + + const { group, area } = ev.currentTarget as unknown as { + group: AreaStrategyGroup; + area: string; + }; + + const newConfig: AreasDashboardStrategyConfig = { + ...this._config!, + areas_options: { + ...this._config!.areas_options, + [area]: { + ...this._config!.areas_options?.[area], + groups_options: { + ...this._config!.areas_options?.[area]?.groups_options, + [group]: value, + }, + }, + }, + }; + + fireEvent(this, "config-changed", { config: newConfig }); + } + + static get styles() { + return [ + css` + .toolbar { + display: flex; + align-items: center; + } + ha-expansion-panel { + margin-bottom: 8px; + } + `, + ]; + } } declare global { diff --git a/src/panels/lovelace/strategies/areas/helpers/area-strategy-helper.ts b/src/panels/lovelace/strategies/areas/helpers/area-strategy-helper.ts new file mode 100644 index 0000000000..ad560773c6 --- /dev/null +++ b/src/panels/lovelace/strategies/areas/helpers/area-strategy-helper.ts @@ -0,0 +1,149 @@ +import type { EntityFilterFunc } from "../../../../../common/entity/entity_filter"; +import { generateEntityFilter } from "../../../../../common/entity/entity_filter"; +import { orderCompare } from "../../../../../common/string/compare"; +import type { HomeAssistant } from "../../../../../types"; + +export const AREA_STRATEGY_GROUPS = [ + "lights", + "climate", + "media_players", + "security", +] as const; + +export const AREA_STRATEGY_GROUP_ICONS = { + lights: "mdi:lightbulb", + climate: "mdi:home-thermometer", + media_players: "mdi:multimedia", + security: "mdi:security", +}; + +// Todo be replace by translation when validated +export const AREA_STRATEGY_GROUP_LABELS = { + lights: "Lights", + climate: "Climate", + media_players: "Entertainment", + security: "Security", +}; + +export type AreaStrategyGroup = (typeof AREA_STRATEGY_GROUPS)[number]; + +type AreaEntitiesByGroup = Record; + +type AreaFilteredByGroup = Record; + +interface DisplayOptions { + hidden?: string[]; + order?: string[]; +} + +type AreaGroupsDisplayOptions = Record; + +export const getAreaGroupedEntities = ( + area: string, + hass: HomeAssistant, + displayOptions?: AreaGroupsDisplayOptions +): AreaEntitiesByGroup => { + const allEntities = Object.keys(hass.states); + + const groupedFilters: AreaFilteredByGroup = { + lights: [ + generateEntityFilter(hass, { + domain: "light", + area: area, + entity_category: "none", + }), + ], + climate: [ + generateEntityFilter(hass, { + domain: "climate", + area: area, + entity_category: "none", + }), + generateEntityFilter(hass, { + domain: "humidifier", + area: area, + entity_category: "none", + }), + generateEntityFilter(hass, { + domain: "cover", + area: area, + device_class: [ + "shutter", + "awning", + "blind", + "curtain", + "shade", + "shutter", + "window", + ], + entity_category: "none", + }), + generateEntityFilter(hass, { + domain: "binary_sensor", + area: area, + device_class: "window", + entity_category: "none", + }), + ], + media_players: [ + generateEntityFilter(hass, { + domain: "media_player", + area: area, + entity_category: "none", + }), + ], + security: [ + generateEntityFilter(hass, { + domain: "alarm_control_panel", + area: area, + entity_category: "none", + }), + generateEntityFilter(hass, { + domain: "lock", + area: area, + entity_category: "none", + }), + generateEntityFilter(hass, { + domain: "cover", + device_class: ["door", "garage", "gate"], + area: area, + entity_category: "none", + }), + generateEntityFilter(hass, { + domain: "binary_sensor", + device_class: ["door", "garage_door"], + area: area, + entity_category: "none", + }), + ], + }; + + return Object.fromEntries( + Object.entries(groupedFilters).map(([group, filters]) => { + const entities = filters.reduce( + (acc, filter) => [ + ...acc, + ...allEntities.filter((entity) => filter(entity)), + ], + [] + ); + + const hidden = displayOptions?.[group]?.hidden + ? new Set(displayOptions[group].hidden) + : undefined; + + const order = displayOptions?.[group]?.order; + + let filteredEntities = entities; + if (hidden) { + filteredEntities = entities.filter( + (entity: string) => !hidden.has(entity) + ); + } + if (order) { + filteredEntities = filteredEntities.concat().sort(orderCompare(order)); + } + return [group, filteredEntities]; + }) + ) as AreaEntitiesByGroup; +}; diff --git a/src/panels/lovelace/strategies/get-strategy.ts b/src/panels/lovelace/strategies/get-strategy.ts index f39b2b9302..f29259f0de 100644 --- a/src/panels/lovelace/strategies/get-strategy.ts +++ b/src/panels/lovelace/strategies/get-strategy.ts @@ -32,7 +32,7 @@ const STRATEGIES: Record> = { energy: () => import("../../energy/strategies/energy-view-strategy"), map: () => import("./map/map-view-strategy"), iframe: () => import("./iframe/iframe-view-strategy"), - area: () => import("./area/area-view-strategy"), + area: () => import("./areas/area-view-strategy"), areas: () => import("./areas/areas-view-strategy"), }, section: {},