diff --git a/src/common/areas/areas-floor-hierarchy.ts b/src/common/areas/areas-floor-hierarchy.ts new file mode 100644 index 000000000000..33c1550f4817 --- /dev/null +++ b/src/common/areas/areas-floor-hierarchy.ts @@ -0,0 +1,53 @@ +import type { AreaRegistryEntry } from "../../data/area_registry"; +import type { FloorRegistryEntry } from "../../data/floor_registry"; + +export interface AreasFloorHierarchy { + floors: { + id: string; + areas: string[]; + }[]; + areas: string[]; +} + +export const getAreasFloorHierarchy = ( + floors: FloorRegistryEntry[], + areas: AreaRegistryEntry[] +): AreasFloorHierarchy => { + const floorAreas = new Map(); + const unassignedAreas: string[] = []; + + for (const area of areas) { + if (area.floor_id) { + if (!floorAreas.has(area.floor_id)) { + floorAreas.set(area.floor_id, []); + } + floorAreas.get(area.floor_id)!.push(area.area_id); + } else { + unassignedAreas.push(area.area_id); + } + } + + const hierarchy: AreasFloorHierarchy = { + floors: floors.map((floor) => ({ + id: floor.floor_id, + areas: floorAreas.get(floor.floor_id) || [], + })), + areas: unassignedAreas, + }; + + return hierarchy; +}; + +export const getAreasOrder = (hierarchy: AreasFloorHierarchy): string[] => { + const order: string[] = []; + + for (const floor of hierarchy.floors) { + order.push(...floor.areas); + } + order.push(...hierarchy.areas); + + return order; +}; + +export const getFloorOrder = (hierarchy: AreasFloorHierarchy): string[] => + hierarchy.floors.map((floor) => floor.id); diff --git a/src/data/area_floor.ts b/src/data/area_floor.ts index 286a5de4e3b8..5bda9506a3b9 100644 --- a/src/data/area_floor.ts +++ b/src/data/area_floor.ts @@ -1,3 +1,4 @@ +import { getAreasFloorHierarchy } from "../common/areas/areas-floor-hierarchy"; import { computeAreaName } from "../common/entity/compute_area_name"; import { computeDomain } from "../common/entity/compute_domain"; import { computeFloorName } from "../common/entity/compute_floor_name"; @@ -12,11 +13,7 @@ import { } from "./device_registry"; import type { HaEntityPickerEntityFilterFunc } from "./entity"; import type { EntityRegistryDisplayEntry } from "./entity_registry"; -import { - floorCompare, - getFloorAreaLookup, - type FloorRegistryEntry, -} from "./floor_registry"; +import type { FloorRegistryEntry } from "./floor_registry"; export interface FloorComboBoxItem extends PickerComboBoxItem { type: "floor" | "area"; @@ -182,68 +179,59 @@ export const getAreasAndFloors = ( ); } - const floorAreaLookup = getFloorAreaLookup(outputAreas); - const unassignedAreas = Object.values(outputAreas).filter( - (area) => !area.floor_id || !floorAreaLookup[area.floor_id] - ); - - const compare = floorCompare(haFloors); - - // @ts-ignore - const floorAreaEntries: [ - FloorRegistryEntry | undefined, - AreaRegistryEntry[], - ][] = Object.entries(floorAreaLookup) - .map(([floorId, floorAreas]) => { - const floor = floors.find((fl) => fl.floor_id === floorId)!; - return [floor, floorAreas] as const; - }) - .sort(([floorA], [floorB]) => compare(floorA.floor_id, floorB.floor_id)); + const hierarchy = getAreasFloorHierarchy(floors, outputAreas); const items: FloorComboBoxItem[] = []; - floorAreaEntries.forEach(([floor, floorAreas]) => { - if (floor) { - const floorName = computeFloorName(floor); + hierarchy.floors.forEach((f) => { + const floor = haFloors[f.id]; + const floorAreas = f.areas.map((areaId) => haAreas[areaId]); - const areaSearchLabels = floorAreas - .map((area) => { - const areaName = computeAreaName(area) || area.area_id; - return [area.area_id, areaName, ...area.aliases]; - }) - .flat(); + const floorName = computeFloorName(floor); + + const areaSearchLabels = floorAreas + .map((area) => { + const areaName = computeAreaName(area); + return [area.area_id, ...(areaName ? [areaName] : []), ...area.aliases]; + }) + .flat(); + + items.push({ + id: formatId({ id: floor.floor_id, type: "floor" }), + type: "floor", + primary: floorName, + floor: floor, + icon: floor.icon || undefined, + search_labels: [ + floor.floor_id, + floorName, + ...floor.aliases, + ...areaSearchLabels, + ], + }); - items.push({ - id: formatId({ id: floor.floor_id, type: "floor" }), - type: "floor", - primary: floorName, - floor: floor, - icon: floor.icon || undefined, - search_labels: [ - floor.floor_id, - floorName, - ...floor.aliases, - ...areaSearchLabels, - ], - }); - } items.push( ...floorAreas.map((area) => { - const areaName = computeAreaName(area) || area.area_id; + const areaName = computeAreaName(area); return { id: formatId({ id: area.area_id, type: "area" }), type: "area" as const, - primary: areaName, + primary: areaName || area.area_id, area: area, icon: area.icon || undefined, - search_labels: [area.area_id, areaName, ...area.aliases], + search_labels: [ + area.area_id, + ...(areaName ? [areaName] : []), + ...area.aliases, + ], }; }) ); }); items.push( - ...unassignedAreas.map((area) => { + ...hierarchy.areas.map((areaId) => { + const area = haAreas[areaId]; const areaName = computeAreaName(area) || area.area_id; return { id: formatId({ id: area.area_id, type: "area" }), diff --git a/src/data/area_registry.ts b/src/data/area_registry.ts index 2fca94fcaae5..212143784a13 100644 --- a/src/data/area_registry.ts +++ b/src/data/area_registry.ts @@ -59,6 +59,15 @@ export const deleteAreaRegistryEntry = (hass: HomeAssistant, areaId: string) => area_id: areaId, }); +export const reorderAreaRegistryEntries = ( + hass: HomeAssistant, + areaIds: string[] +) => + hass.callWS({ + type: "config/area_registry/reorder", + area_ids: areaIds, + }); + export const getAreaEntityLookup = ( entities: EntityRegistryEntry[] ): AreaEntityLookup => { diff --git a/src/data/floor_registry.ts b/src/data/floor_registry.ts index 8bff8df5a3d7..a281223bb069 100644 --- a/src/data/floor_registry.ts +++ b/src/data/floor_registry.ts @@ -51,6 +51,15 @@ export const deleteFloorRegistryEntry = ( floor_id: floorId, }); +export const reorderFloorRegistryEntries = ( + hass: HomeAssistant, + floorIds: string[] +) => + hass.callWS({ + type: "config/floor_registry/reorder", + floor_ids: floorIds, + }); + export const getFloorAreaLookup = ( areas: AreaRegistryEntry[] ): FloorAreaLookup => { diff --git a/src/panels/climate/strategies/climate-view-strategy.ts b/src/panels/climate/strategies/climate-view-strategy.ts index 26abc9a3bf86..53f0e0d3621d 100644 --- a/src/panels/climate/strategies/climate-view-strategy.ts +++ b/src/panels/climate/strategies/climate-view-strategy.ts @@ -1,21 +1,17 @@ import { ReactiveElement } from "lit"; import { customElement } from "lit/decorators"; +import { getAreasFloorHierarchy } from "../../../common/areas/areas-floor-hierarchy"; import { findEntities, generateEntityFilter, type EntityFilter, } from "../../../common/entity/entity_filter"; +import { floorDefaultIcon } from "../../../components/ha-floor-icon"; import type { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section"; import type { LovelaceViewConfig } from "../../../data/lovelace/config/view"; import type { HomeAssistant } from "../../../types"; -import { - computeAreaTileCardConfig, - getAreas, - getFloors, -} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper"; -import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure"; -import { floorDefaultIcon } from "../../../components/ha-floor-icon"; +import { computeAreaTileCardConfig } from "../../lovelace/strategies/areas/helpers/areas-strategy-helper"; export interface ClimateViewStrategyConfig { type: "climate"; @@ -139,9 +135,9 @@ export class ClimateViewStrategy extends ReactiveElement { _config: ClimateViewStrategyConfig, hass: HomeAssistant ): Promise { - const areas = getAreas(hass.areas); - const floors = getFloors(hass.floors); - const home = getHomeStructure(floors, areas); + const areas = Object.values(hass.areas); + const floors = Object.values(hass.floors); + const hierarchy = getAreasFloorHierarchy(floors, areas); const sections: LovelaceSectionRawConfig[] = []; @@ -153,10 +149,11 @@ export class ClimateViewStrategy extends ReactiveElement { const entities = findEntities(allEntities, climateFilters); - const floorCount = home.floors.length + (home.areas.length ? 1 : 0); + const floorCount = + hierarchy.floors.length + (hierarchy.areas.length ? 1 : 0); // Process floors - for (const floorStructure of home.floors) { + for (const floorStructure of hierarchy.floors) { const floorId = floorStructure.id; const areaIds = floorStructure.areas; const floor = hass.floors[floorId]; @@ -185,7 +182,7 @@ export class ClimateViewStrategy extends ReactiveElement { } // Process unassigned areas - if (home.areas.length > 0) { + if (hierarchy.areas.length > 0) { const section: LovelaceSectionRawConfig = { type: "grid", column_span: 2, @@ -200,7 +197,7 @@ export class ClimateViewStrategy extends ReactiveElement { ], }; - const areaCards = processAreasForClimate(home.areas, hass, entities); + const areaCards = processAreasForClimate(hierarchy.areas, hass, entities); if (areaCards.length > 0) { section.cards!.push(...areaCards); diff --git a/src/panels/config/areas/ha-config-areas-dashboard.ts b/src/panels/config/areas/ha-config-areas-dashboard.ts index 09477570683c..79fdc1c70c1e 100644 --- a/src/panels/config/areas/ha-config-areas-dashboard.ts +++ b/src/panels/config/areas/ha-config-areas-dashboard.ts @@ -2,38 +2,47 @@ import type { ActionDetail } from "@material/mwc-list"; import { mdiDelete, mdiDotsVertical, + mdiDragHorizontalVariant, mdiHelpCircle, mdiPencil, mdiPlus, } from "@mdi/js"; import { - LitElement, - type PropertyValues, - type TemplateResult, css, html, + LitElement, nothing, + type PropertyValues, + type TemplateResult, } from "lit"; import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; +import { + getAreasFloorHierarchy, + getAreasOrder, + getFloorOrder, + type AreasFloorHierarchy, +} from "../../../common/areas/areas-floor-hierarchy"; import { formatListWithAnds } from "../../../common/string/format-list"; import "../../../components/ha-fab"; import "../../../components/ha-floor-icon"; import "../../../components/ha-icon-button"; import "../../../components/ha-list-item"; import "../../../components/ha-sortable"; +import type { HaSortableOptions } from "../../../components/ha-sortable"; import "../../../components/ha-svg-icon"; import type { AreaRegistryEntry } from "../../../data/area_registry"; import { createAreaRegistryEntry, + reorderAreaRegistryEntries, updateAreaRegistryEntry, } from "../../../data/area_registry"; import type { FloorRegistryEntry } from "../../../data/floor_registry"; import { createFloorRegistryEntry, deleteFloorRegistryEntry, - getFloorAreaLookup, + reorderFloorRegistryEntries, updateFloorRegistryEntry, } from "../../../data/floor_registry"; import { @@ -42,6 +51,7 @@ import { } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-tabs-subpage"; import type { HomeAssistant, Route } from "../../../types"; +import { showToast } from "../../../util/toast"; import "../ha-config-section"; import { configSections } from "../ha-panel-config"; import { @@ -52,7 +62,17 @@ import { showFloorRegistryDetailDialog } from "./show-dialog-floor-registry-deta const UNASSIGNED_FLOOR = "__unassigned__"; -const SORT_OPTIONS = { sort: false, delay: 500, delayOnTouchOnly: true }; +const SORT_OPTIONS: HaSortableOptions = { + sort: true, + delay: 500, + delayOnTouchOnly: true, +}; + +interface AreaStats { + devices: number; + services: number; + entities: number; +} @customElement("ha-config-areas-dashboard") export class HaConfigAreasDashboard extends LitElement { @@ -64,55 +84,50 @@ export class HaConfigAreasDashboard extends LitElement { @property({ attribute: false }) public route!: Route; - @state() private _areas: AreaRegistryEntry[] = []; + @state() private _hierarchy?: AreasFloorHierarchy; + + private _blockHierarchyUpdate = false; - private _processAreas = memoizeOne( + private _blockHierarchyUpdateTimeout?: number; + + private _processAreasStats = memoizeOne( ( - areas: AreaRegistryEntry[], + areas: HomeAssistant["areas"], devices: HomeAssistant["devices"], - entities: HomeAssistant["entities"], - floors: HomeAssistant["floors"] - ) => { - const processArea = (area: AreaRegistryEntry) => { - let noDevicesInArea = 0; - let noServicesInArea = 0; - let noEntitiesInArea = 0; + entities: HomeAssistant["entities"] + ): Map => { + const computeAreaStats = (area: AreaRegistryEntry) => { + let devicesCount = 0; + let servicesCount = 0; + let entitiesCount = 0; for (const device of Object.values(devices)) { if (device.area_id === area.area_id) { if (device.entry_type === "service") { - noServicesInArea++; + servicesCount++; } else { - noDevicesInArea++; + devicesCount++; } } } for (const entity of Object.values(entities)) { if (entity.area_id === area.area_id) { - noEntitiesInArea++; + entitiesCount++; } } return { - ...area, - devices: noDevicesInArea, - services: noServicesInArea, - entities: noEntitiesInArea, + devices: devicesCount, + services: servicesCount, + entities: entitiesCount, }; }; - - const floorAreaLookup = getFloorAreaLookup(areas); - const unassignedAreas = areas.filter( - (area) => !area.floor_id || !floorAreaLookup[area.floor_id] - ); - return { - floors: Object.values(floors).map((floor) => ({ - ...floor, - areas: (floorAreaLookup[floor.floor_id] || []).map(processArea), - })), - unassignedAreas: unassignedAreas.map(processArea), - }; + const areaStats = new Map(); + Object.values(areas).forEach((area) => { + areaStats.set(area.area_id, computeAreaStats(area)); + }); + return areaStats; } ); @@ -120,25 +135,32 @@ export class HaConfigAreasDashboard extends LitElement { super.willUpdate(changedProperties); if (changedProperties.has("hass")) { const oldHass = changedProperties.get("hass"); - if (this.hass.areas !== oldHass?.areas) { - this._areas = Object.values(this.hass.areas); + if ( + (this.hass.areas !== oldHass?.areas || + this.hass.floors !== oldHass?.floors) && + !this._blockHierarchyUpdate + ) { + this._computeHierarchy(); } } } - protected render(): TemplateResult { - const areasAndFloors = - !this.hass.areas || - !this.hass.devices || - !this.hass.entities || - !this.hass.floors - ? undefined - : this._processAreas( - this._areas, - this.hass.devices, - this.hass.entities, - this.hass.floors - ); + private _computeHierarchy() { + this._hierarchy = getAreasFloorHierarchy( + Object.values(this.hass.floors), + Object.values(this.hass.areas) + ); + } + + protected render(): TemplateResult<1> | typeof nothing { + if (!this._hierarchy) { + return nothing; + } + const areasStats = this._processAreasStats( + this.hass.areas, + this.hass.devices, + this.hass.entities + ); return html`
- ${areasAndFloors?.floors.map( - (floor) => - html`
-
-

- - ${floor.name} -

- - - ${this.hass.localize( - "ui.panel.config.areas.picker.floor.edit_floor" - )} +
+ ${this._hierarchy.floors.map(({ areas, id }) => { + const floor = this.hass.floors[id]; + if (!floor) { + return nothing; + } + return html` +
+
+

+ + ${floor.name} +

+
+ + + + ${this.hass.localize( + "ui.panel.config.areas.picker.floor.edit_floor" + )} + ${this.hass.localize( + "ui.panel.config.areas.picker.floor.delete_floor" + )} + +
+
+ - ${this.hass.localize( - "ui.panel.config.areas.picker.floor.delete_floor" - )} - -
- -
- ${floor.areas.map((area) => this._renderArea(area))} +
+ ${areas.map((areaId) => { + const area = this.hass.areas[areaId]; + if (!area) { + return nothing; + } + const stats = areasStats.get(area.area_id); + return this._renderArea(area, stats); + })} +
+
-
-
` - )} - ${areasAndFloors?.unassignedAreas.length - ? html`
-
-

- ${this.hass.localize( - "ui.panel.config.areas.picker.unassigned_areas" - )} -

-
- -
- ${areasAndFloors?.unassignedAreas.map((area) => - this._renderArea(area) - )} + `; + })} +
+
+ + ${this._hierarchy.areas.length + ? html` +
+
+

+ ${this.hass.localize( + "ui.panel.config.areas.picker.unassigned_areas" + )} +

- -
` + +
+ ${this._hierarchy.areas.map((areaId) => { + const area = this.hass.areas[areaId]; + if (!area) { + return nothing; + } + const stats = areasStats.get(area.area_id); + return this._renderArea(area, stats); + })} +
+
+
+ ` : nothing}
- -
- ${!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)) - )} + private _renderArea( + area: AreaRegistryEntry, + stats: AreaStats | undefined + ): TemplateResult<1> { + return html` + + +
+ ${!area.picture && area.icon + ? html`` + : ""}
-
- - `; +
+ ${area.name} + +
+
+
+ ${formatListWithAnds( + this.hass.locale, + [ + stats?.devices && + this.hass.localize( + "ui.panel.config.integrations.config_entry.devices", + { count: stats.devices } + ), + stats?.services && + this.hass.localize( + "ui.panel.config.integrations.config_entry.services", + { count: stats.services } + ), + stats?.entities && + this.hass.localize( + "ui.panel.config.integrations.config_entry.entities", + { count: stats.entities } + ), + ].filter((v): v is string => Boolean(v)) + )} +
+
+ + + `; } protected firstUpdated(changedProps) { @@ -326,24 +391,170 @@ export class HaConfigAreasDashboard extends LitElement { }); } + private async _floorMoved(ev) { + ev.stopPropagation(); + if (!this.hass || !this._hierarchy) { + return; + } + const { oldIndex, newIndex } = ev.detail; + + const reorderFloors = ( + floors: AreasFloorHierarchy["floors"], + oldIdx: number, + newIdx: number + ) => { + const newFloors = [...floors]; + const [movedFloor] = newFloors.splice(oldIdx, 1); + newFloors.splice(newIdx, 0, movedFloor); + return newFloors; + }; + + // Optimistically update UI + this._hierarchy = { + ...this._hierarchy, + floors: reorderFloors(this._hierarchy.floors, oldIndex, newIndex), + }; + + const areaOrder = getAreasOrder(this._hierarchy); + const floorOrder = getFloorOrder(this._hierarchy); + + // Block hierarchy updates for 500ms to avoid flickering + // because of multiple async updates + this._blockHierarchyUpdateFor(500); + + try { + await reorderAreaRegistryEntries(this.hass, areaOrder); + await reorderFloorRegistryEntries(this.hass, floorOrder); + } catch { + showToast(this, { + message: this.hass.localize( + "ui.panel.config.areas.picker.floor_reorder_failed" + ), + }); + // Revert on error + this._computeHierarchy(); + } + } + + private async _areaMoved(ev) { + ev.stopPropagation(); + if (!this.hass || !this._hierarchy) { + return; + } + const { floor } = ev.currentTarget; + const { oldIndex, newIndex } = ev.detail; + + const floorId = floor === UNASSIGNED_FLOOR ? null : floor; + + // Reorder areas within the same floor + const reorderAreas = (areas: string[], oldIdx: number, newIdx: number) => { + const newAreas = [...areas]; + const [movedArea] = newAreas.splice(oldIdx, 1); + newAreas.splice(newIdx, 0, movedArea); + return newAreas; + }; + + // Optimistically update UI + this._hierarchy = { + ...this._hierarchy, + floors: this._hierarchy.floors.map((f) => { + if (f.id === floorId) { + return { + ...f, + areas: reorderAreas(f.areas, oldIndex, newIndex), + }; + } + return f; + }), + areas: + floorId === null + ? reorderAreas(this._hierarchy.areas, oldIndex, newIndex) + : this._hierarchy.areas, + }; + + const areaOrder = getAreasOrder(this._hierarchy); + + try { + await reorderAreaRegistryEntries(this.hass, areaOrder); + } catch { + showToast(this, { + message: this.hass.localize( + "ui.panel.config.areas.picker.area_move_failed" + ), + }); + // Revert on error + this._computeHierarchy(); + } + } + private async _areaAdded(ev) { ev.stopPropagation(); + if (!this.hass || !this._hierarchy) { + return; + } const { floor } = ev.currentTarget; + const { data: area, index } = ev.detail; const newFloorId = floor === UNASSIGNED_FLOOR ? null : floor; - const { data: area } = ev.detail; - - this._areas = this._areas.map((a) => { - if (a.area_id === area.area_id) { - return { ...a, floor_id: newFloorId }; - } - return a; - }); + // Insert area at the specified index + const insertAtIndex = (areas: string[], areaId: string, idx: number) => { + const newAreas = [...areas]; + newAreas.splice(idx, 0, areaId); + return newAreas; + }; + + // Optimistically update UI + this._hierarchy = { + ...this._hierarchy, + floors: this._hierarchy.floors.map((f) => { + if (f.id === newFloorId) { + return { + ...f, + areas: insertAtIndex(f.areas, area.area_id, index), + }; + } + return { + ...f, + areas: f.areas.filter((id) => id !== area.area_id), + }; + }), + areas: + newFloorId === null + ? insertAtIndex(this._hierarchy.areas, area.area_id, index) + : this._hierarchy.areas.filter((id) => id !== area.area_id), + }; + + const areaOrder = getAreasOrder(this._hierarchy); + + // Block hierarchy updates for 500ms to avoid flickering + // because of multiple async updates + this._blockHierarchyUpdateFor(500); + + try { + await reorderAreaRegistryEntries(this.hass, areaOrder); + await updateAreaRegistryEntry(this.hass, area.area_id, { + floor_id: newFloorId, + }); + } catch { + showToast(this, { + message: this.hass.localize( + "ui.panel.config.areas.picker.area_move_failed" + ), + }); + // Revert on error + this._computeHierarchy(); + } + } - await updateAreaRegistryEntry(this.hass, area.area_id, { - floor_id: newFloorId, - }); + private _blockHierarchyUpdateFor(time: number) { + this._blockHierarchyUpdate = true; + if (this._blockHierarchyUpdateTimeout) { + window.clearTimeout(this._blockHierarchyUpdateTimeout); + } + this._blockHierarchyUpdateTimeout = window.setTimeout(() => { + this._blockHierarchyUpdate = false; + }, time); } private _handleFloorAction(ev: CustomEvent) { @@ -463,6 +674,10 @@ export class HaConfigAreasDashboard extends LitElement { .header ha-icon { margin-inline-end: 8px; } + .header .actions { + display: flex; + align-items: center; + } .areas { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); @@ -473,6 +688,10 @@ export class HaConfigAreasDashboard extends LitElement { .areas > * { max-width: 500px; } + .handle { + cursor: move; /* fallback if grab cursor is unsupported */ + cursor: grab; + } ha-card { overflow: hidden; } diff --git a/src/panels/light/strategies/light-view-strategy.ts b/src/panels/light/strategies/light-view-strategy.ts index b3de88f1cffb..571ca51b7259 100644 --- a/src/panels/light/strategies/light-view-strategy.ts +++ b/src/panels/light/strategies/light-view-strategy.ts @@ -1,5 +1,6 @@ import { ReactiveElement } from "lit"; import { customElement } from "lit/decorators"; +import { getAreasFloorHierarchy } from "../../../common/areas/areas-floor-hierarchy"; import { findEntities, generateEntityFilter, @@ -10,12 +11,7 @@ import type { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section"; import type { LovelaceViewConfig } from "../../../data/lovelace/config/view"; import type { HomeAssistant } from "../../../types"; -import { - computeAreaTileCardConfig, - getAreas, - getFloors, -} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper"; -import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure"; +import { computeAreaTileCardConfig } from "../../lovelace/strategies/areas/helpers/areas-strategy-helper"; export interface LightViewStrategyConfig { type: "light"; @@ -85,9 +81,9 @@ export class LightViewStrategy extends ReactiveElement { _config: LightViewStrategyConfig, hass: HomeAssistant ): Promise { - const areas = getAreas(hass.areas); - const floors = getFloors(hass.floors); - const home = getHomeStructure(floors, areas); + const areas = Object.values(hass.areas); + const floors = Object.values(hass.floors); + const hierarchy = getAreasFloorHierarchy(floors, areas); const sections: LovelaceSectionRawConfig[] = []; @@ -99,10 +95,11 @@ export class LightViewStrategy extends ReactiveElement { const entities = findEntities(allEntities, lightFilters); - const floorCount = home.floors.length + (home.areas.length ? 1 : 0); + const floorCount = + hierarchy.floors.length + (hierarchy.areas.length ? 1 : 0); // Process floors - for (const floorStructure of home.floors) { + for (const floorStructure of hierarchy.floors) { const floorId = floorStructure.id; const areaIds = floorStructure.areas; const floor = hass.floors[floorId]; @@ -131,7 +128,7 @@ export class LightViewStrategy extends ReactiveElement { } // Process unassigned areas - if (home.areas.length > 0) { + if (hierarchy.areas.length > 0) { const section: LovelaceSectionRawConfig = { type: "grid", column_span: 2, @@ -146,7 +143,7 @@ export class LightViewStrategy extends ReactiveElement { ], }; - const areaCards = processAreasForLight(home.areas, hass, entities); + const areaCards = processAreasForLight(hierarchy.areas, hass, entities); if (areaCards.length > 0) { section.cards!.push(...areaCards); diff --git a/src/panels/lovelace/strategies/home/helpers/home-structure.ts b/src/panels/lovelace/strategies/home/helpers/home-structure.ts deleted file mode 100644 index 59bf033b6f5f..000000000000 --- a/src/panels/lovelace/strategies/home/helpers/home-structure.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { AreaRegistryEntry } from "../../../../../data/area_registry"; -import type { FloorRegistryEntry } from "../../../../../data/floor_registry"; - -interface HomeStructure { - floors: { - id: string; - areas: string[]; - }[]; - areas: string[]; -} - -export const getHomeStructure = ( - floors: FloorRegistryEntry[], - areas: AreaRegistryEntry[] -): HomeStructure => { - const floorAreas = new Map(); - const unassignedAreas: string[] = []; - - for (const area of areas) { - if (area.floor_id) { - if (!floorAreas.has(area.floor_id)) { - floorAreas.set(area.floor_id, []); - } - floorAreas.get(area.floor_id)!.push(area.area_id); - } else { - unassignedAreas.push(area.area_id); - } - } - - const homeStructure: HomeStructure = { - floors: floors.map((floor) => ({ - id: floor.floor_id, - areas: floorAreas.get(floor.floor_id) || [], - })), - areas: unassignedAreas, - }; - - return homeStructure; -}; diff --git a/src/panels/lovelace/strategies/home/home-dashboard-strategy.ts b/src/panels/lovelace/strategies/home/home-dashboard-strategy.ts index 0bc19702240a..662988260804 100644 --- a/src/panels/lovelace/strategies/home/home-dashboard-strategy.ts +++ b/src/panels/lovelace/strategies/home/home-dashboard-strategy.ts @@ -4,7 +4,6 @@ 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 { getAreas } from "../areas/helpers/areas-strategy-helper"; import type { LovelaceStrategyEditor } from "../types"; import { getSummaryLabel, @@ -46,7 +45,7 @@ export class HomeDashboardStrategy extends ReactiveElement { }; } - const areas = getAreas(hass.areas); + const areas = Object.values(hass.areas); const areaViews = areas.map((area) => { const path = `areas-${area.area_id}`; diff --git a/src/panels/lovelace/strategies/home/home-main-view-strategy.ts b/src/panels/lovelace/strategies/home/home-main-view-strategy.ts index 23acca02c58a..0f03f98ce793 100644 --- a/src/panels/lovelace/strategies/home/home-main-view-strategy.ts +++ b/src/panels/lovelace/strategies/home/home-main-view-strategy.ts @@ -22,9 +22,8 @@ import type { MarkdownCardConfig, WeatherForecastCardConfig, } from "../../cards/types"; -import { getAreas, getFloors } from "../areas/helpers/areas-strategy-helper"; import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy"; -import { getHomeStructure } from "./helpers/home-structure"; +import { getAreasFloorHierarchy } from "../../../../common/areas/areas-floor-hierarchy"; import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries"; export interface HomeMainViewStrategyConfig { @@ -64,10 +63,10 @@ export class HomeMainViewStrategy extends ReactiveElement { config: HomeMainViewStrategyConfig, hass: HomeAssistant ): Promise { - const areas = getAreas(hass.areas); - const floors = getFloors(hass.floors); + const areas = Object.values(hass.areas); + const floors = Object.values(hass.floors); - const home = getHomeStructure(floors, areas); + const home = getAreasFloorHierarchy(floors, areas); const floorCount = home.floors.length + (home.areas.length ? 1 : 0); diff --git a/src/panels/lovelace/strategies/home/home-media-players-view-strategy.ts b/src/panels/lovelace/strategies/home/home-media-players-view-strategy.ts index 04573e79bb55..09544d522eca 100644 --- a/src/panels/lovelace/strategies/home/home-media-players-view-strategy.ts +++ b/src/panels/lovelace/strategies/home/home-media-players-view-strategy.ts @@ -10,8 +10,7 @@ import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/ import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view"; import type { HomeAssistant } from "../../../../types"; import type { MediaControlCardConfig } from "../../cards/types"; -import { getAreas, getFloors } from "../areas/helpers/areas-strategy-helper"; -import { getHomeStructure } from "./helpers/home-structure"; +import { getAreasFloorHierarchy } from "../../../../common/areas/areas-floor-hierarchy"; import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries"; export interface HomeMediaPlayersViewStrategyConfig { @@ -85,9 +84,9 @@ export class HomeMMediaPlayersViewStrategy extends ReactiveElement { _config: HomeMediaPlayersViewStrategyConfig, hass: HomeAssistant ): Promise { - const areas = getAreas(hass.areas); - const floors = getFloors(hass.floors); - const home = getHomeStructure(floors, areas); + const areas = Object.values(hass.areas); + const floors = Object.values(hass.floors); + const home = getAreasFloorHierarchy(floors, areas); const sections: LovelaceSectionRawConfig[] = []; diff --git a/src/panels/security/strategies/security-view-strategy.ts b/src/panels/security/strategies/security-view-strategy.ts index 18cb22b184b7..da4269bc6a9f 100644 --- a/src/panels/security/strategies/security-view-strategy.ts +++ b/src/panels/security/strategies/security-view-strategy.ts @@ -1,5 +1,6 @@ import { ReactiveElement } from "lit"; import { customElement } from "lit/decorators"; +import { getAreasFloorHierarchy } from "../../../common/areas/areas-floor-hierarchy"; import { findEntities, generateEntityFilter, @@ -10,12 +11,7 @@ import type { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section"; import type { LovelaceViewConfig } from "../../../data/lovelace/config/view"; import type { HomeAssistant } from "../../../types"; -import { - computeAreaTileCardConfig, - getAreas, - getFloors, -} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper"; -import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure"; +import { computeAreaTileCardConfig } from "../../lovelace/strategies/areas/helpers/areas-strategy-helper"; export interface SecurityViewStrategyConfig { type: "security"; @@ -127,9 +123,9 @@ export class SecurityViewStrategy extends ReactiveElement { _config: SecurityViewStrategyConfig, hass: HomeAssistant ): Promise { - const areas = getAreas(hass.areas); - const floors = getFloors(hass.floors); - const home = getHomeStructure(floors, areas); + const areas = Object.values(hass.areas); + const floors = Object.values(hass.floors); + const hierarchy = getAreasFloorHierarchy(floors, areas); const sections: LovelaceSectionRawConfig[] = []; @@ -141,10 +137,11 @@ export class SecurityViewStrategy extends ReactiveElement { const entities = findEntities(allEntities, securityFilters); - const floorCount = home.floors.length + (home.areas.length ? 1 : 0); + const floorCount = + hierarchy.floors.length + (hierarchy.areas.length ? 1 : 0); // Process floors - for (const floorStructure of home.floors) { + for (const floorStructure of hierarchy.floors) { const floorId = floorStructure.id; const areaIds = floorStructure.areas; const floor = hass.floors[floorId]; @@ -173,7 +170,7 @@ export class SecurityViewStrategy extends ReactiveElement { } // Process unassigned areas - if (home.areas.length > 0) { + if (hierarchy.areas.length > 0) { const section: LovelaceSectionRawConfig = { type: "grid", column_span: 2, @@ -188,7 +185,11 @@ export class SecurityViewStrategy extends ReactiveElement { ], }; - const areaCards = processAreasForSecurity(home.areas, hass, entities); + const areaCards = processAreasForSecurity( + hierarchy.areas, + hass, + entities + ); if (areaCards.length > 0) { section.cards!.push(...areaCards); diff --git a/src/translations/en.json b/src/translations/en.json index 312f3b9726a8..edbf4c514e96 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2456,7 +2456,10 @@ "delete_floor": "Delete floor", "confirm_delete": "Delete floor?", "confirm_delete_text": "Deleting the floor will unassign all areas from it." - } + }, + "area_reorder_failed": "Failed to reorder areas", + "area_move_failed": "Failed to move area", + "floor_reorder_failed": "Failed to reorder floors" }, "editor": { "create_area": "Create area",