Skip to content

Commit e45b631

Browse files
authored
Allow to reorder areas and floors (#27986)
* Add websocket commands * Add area reordering * Reorder floors * Order areas and floors everywhere * Use right area order in area floor picker * Add error handling * Refactor
1 parent ea798cd commit e45b631

File tree

13 files changed

+556
-322
lines changed

13 files changed

+556
-322
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type { AreaRegistryEntry } from "../../data/area_registry";
2+
import type { FloorRegistryEntry } from "../../data/floor_registry";
3+
4+
export interface AreasFloorHierarchy {
5+
floors: {
6+
id: string;
7+
areas: string[];
8+
}[];
9+
areas: string[];
10+
}
11+
12+
export const getAreasFloorHierarchy = (
13+
floors: FloorRegistryEntry[],
14+
areas: AreaRegistryEntry[]
15+
): AreasFloorHierarchy => {
16+
const floorAreas = new Map<string, string[]>();
17+
const unassignedAreas: string[] = [];
18+
19+
for (const area of areas) {
20+
if (area.floor_id) {
21+
if (!floorAreas.has(area.floor_id)) {
22+
floorAreas.set(area.floor_id, []);
23+
}
24+
floorAreas.get(area.floor_id)!.push(area.area_id);
25+
} else {
26+
unassignedAreas.push(area.area_id);
27+
}
28+
}
29+
30+
const hierarchy: AreasFloorHierarchy = {
31+
floors: floors.map((floor) => ({
32+
id: floor.floor_id,
33+
areas: floorAreas.get(floor.floor_id) || [],
34+
})),
35+
areas: unassignedAreas,
36+
};
37+
38+
return hierarchy;
39+
};
40+
41+
export const getAreasOrder = (hierarchy: AreasFloorHierarchy): string[] => {
42+
const order: string[] = [];
43+
44+
for (const floor of hierarchy.floors) {
45+
order.push(...floor.areas);
46+
}
47+
order.push(...hierarchy.areas);
48+
49+
return order;
50+
};
51+
52+
export const getFloorOrder = (hierarchy: AreasFloorHierarchy): string[] =>
53+
hierarchy.floors.map((floor) => floor.id);

src/data/area_floor.ts

Lines changed: 37 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getAreasFloorHierarchy } from "../common/areas/areas-floor-hierarchy";
12
import { computeAreaName } from "../common/entity/compute_area_name";
23
import { computeDomain } from "../common/entity/compute_domain";
34
import { computeFloorName } from "../common/entity/compute_floor_name";
@@ -12,11 +13,7 @@ import {
1213
} from "./device_registry";
1314
import type { HaEntityPickerEntityFilterFunc } from "./entity";
1415
import type { EntityRegistryDisplayEntry } from "./entity_registry";
15-
import {
16-
floorCompare,
17-
getFloorAreaLookup,
18-
type FloorRegistryEntry,
19-
} from "./floor_registry";
16+
import type { FloorRegistryEntry } from "./floor_registry";
2017

2118
export interface FloorComboBoxItem extends PickerComboBoxItem {
2219
type: "floor" | "area";
@@ -182,68 +179,59 @@ export const getAreasAndFloors = (
182179
);
183180
}
184181

185-
const floorAreaLookup = getFloorAreaLookup(outputAreas);
186-
const unassignedAreas = Object.values(outputAreas).filter(
187-
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
188-
);
189-
190-
const compare = floorCompare(haFloors);
191-
192-
// @ts-ignore
193-
const floorAreaEntries: [
194-
FloorRegistryEntry | undefined,
195-
AreaRegistryEntry[],
196-
][] = Object.entries(floorAreaLookup)
197-
.map(([floorId, floorAreas]) => {
198-
const floor = floors.find((fl) => fl.floor_id === floorId)!;
199-
return [floor, floorAreas] as const;
200-
})
201-
.sort(([floorA], [floorB]) => compare(floorA.floor_id, floorB.floor_id));
182+
const hierarchy = getAreasFloorHierarchy(floors, outputAreas);
202183

203184
const items: FloorComboBoxItem[] = [];
204185

205-
floorAreaEntries.forEach(([floor, floorAreas]) => {
206-
if (floor) {
207-
const floorName = computeFloorName(floor);
186+
hierarchy.floors.forEach((f) => {
187+
const floor = haFloors[f.id];
188+
const floorAreas = f.areas.map((areaId) => haAreas[areaId]);
208189

209-
const areaSearchLabels = floorAreas
210-
.map((area) => {
211-
const areaName = computeAreaName(area) || area.area_id;
212-
return [area.area_id, areaName, ...area.aliases];
213-
})
214-
.flat();
190+
const floorName = computeFloorName(floor);
191+
192+
const areaSearchLabels = floorAreas
193+
.map((area) => {
194+
const areaName = computeAreaName(area);
195+
return [area.area_id, ...(areaName ? [areaName] : []), ...area.aliases];
196+
})
197+
.flat();
198+
199+
items.push({
200+
id: formatId({ id: floor.floor_id, type: "floor" }),
201+
type: "floor",
202+
primary: floorName,
203+
floor: floor,
204+
icon: floor.icon || undefined,
205+
search_labels: [
206+
floor.floor_id,
207+
floorName,
208+
...floor.aliases,
209+
...areaSearchLabels,
210+
],
211+
});
215212

216-
items.push({
217-
id: formatId({ id: floor.floor_id, type: "floor" }),
218-
type: "floor",
219-
primary: floorName,
220-
floor: floor,
221-
icon: floor.icon || undefined,
222-
search_labels: [
223-
floor.floor_id,
224-
floorName,
225-
...floor.aliases,
226-
...areaSearchLabels,
227-
],
228-
});
229-
}
230213
items.push(
231214
...floorAreas.map((area) => {
232-
const areaName = computeAreaName(area) || area.area_id;
215+
const areaName = computeAreaName(area);
233216
return {
234217
id: formatId({ id: area.area_id, type: "area" }),
235218
type: "area" as const,
236-
primary: areaName,
219+
primary: areaName || area.area_id,
237220
area: area,
238221
icon: area.icon || undefined,
239-
search_labels: [area.area_id, areaName, ...area.aliases],
222+
search_labels: [
223+
area.area_id,
224+
...(areaName ? [areaName] : []),
225+
...area.aliases,
226+
],
240227
};
241228
})
242229
);
243230
});
244231

245232
items.push(
246-
...unassignedAreas.map((area) => {
233+
...hierarchy.areas.map((areaId) => {
234+
const area = haAreas[areaId];
247235
const areaName = computeAreaName(area) || area.area_id;
248236
return {
249237
id: formatId({ id: area.area_id, type: "area" }),

src/data/area_registry.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ export const deleteAreaRegistryEntry = (hass: HomeAssistant, areaId: string) =>
5959
area_id: areaId,
6060
});
6161

62+
export const reorderAreaRegistryEntries = (
63+
hass: HomeAssistant,
64+
areaIds: string[]
65+
) =>
66+
hass.callWS({
67+
type: "config/area_registry/reorder",
68+
area_ids: areaIds,
69+
});
70+
6271
export const getAreaEntityLookup = (
6372
entities: EntityRegistryEntry[]
6473
): AreaEntityLookup => {

src/data/floor_registry.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ export const deleteFloorRegistryEntry = (
5151
floor_id: floorId,
5252
});
5353

54+
export const reorderFloorRegistryEntries = (
55+
hass: HomeAssistant,
56+
floorIds: string[]
57+
) =>
58+
hass.callWS({
59+
type: "config/floor_registry/reorder",
60+
floor_ids: floorIds,
61+
});
62+
5463
export const getFloorAreaLookup = (
5564
areas: AreaRegistryEntry[]
5665
): FloorAreaLookup => {

src/panels/climate/strategies/climate-view-strategy.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
import { ReactiveElement } from "lit";
22
import { customElement } from "lit/decorators";
3+
import { getAreasFloorHierarchy } from "../../../common/areas/areas-floor-hierarchy";
34
import {
45
findEntities,
56
generateEntityFilter,
67
type EntityFilter,
78
} from "../../../common/entity/entity_filter";
9+
import { floorDefaultIcon } from "../../../components/ha-floor-icon";
810
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
911
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
1012
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
1113
import type { HomeAssistant } from "../../../types";
12-
import {
13-
computeAreaTileCardConfig,
14-
getAreas,
15-
getFloors,
16-
} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
17-
import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure";
18-
import { floorDefaultIcon } from "../../../components/ha-floor-icon";
14+
import { computeAreaTileCardConfig } from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
1915

2016
export interface ClimateViewStrategyConfig {
2117
type: "climate";
@@ -139,9 +135,9 @@ export class ClimateViewStrategy extends ReactiveElement {
139135
_config: ClimateViewStrategyConfig,
140136
hass: HomeAssistant
141137
): Promise<LovelaceViewConfig> {
142-
const areas = getAreas(hass.areas);
143-
const floors = getFloors(hass.floors);
144-
const home = getHomeStructure(floors, areas);
138+
const areas = Object.values(hass.areas);
139+
const floors = Object.values(hass.floors);
140+
const hierarchy = getAreasFloorHierarchy(floors, areas);
145141

146142
const sections: LovelaceSectionRawConfig[] = [];
147143

@@ -153,10 +149,11 @@ export class ClimateViewStrategy extends ReactiveElement {
153149

154150
const entities = findEntities(allEntities, climateFilters);
155151

156-
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
152+
const floorCount =
153+
hierarchy.floors.length + (hierarchy.areas.length ? 1 : 0);
157154

158155
// Process floors
159-
for (const floorStructure of home.floors) {
156+
for (const floorStructure of hierarchy.floors) {
160157
const floorId = floorStructure.id;
161158
const areaIds = floorStructure.areas;
162159
const floor = hass.floors[floorId];
@@ -185,7 +182,7 @@ export class ClimateViewStrategy extends ReactiveElement {
185182
}
186183

187184
// Process unassigned areas
188-
if (home.areas.length > 0) {
185+
if (hierarchy.areas.length > 0) {
189186
const section: LovelaceSectionRawConfig = {
190187
type: "grid",
191188
column_span: 2,
@@ -200,7 +197,7 @@ export class ClimateViewStrategy extends ReactiveElement {
200197
],
201198
};
202199

203-
const areaCards = processAreasForClimate(home.areas, hass, entities);
200+
const areaCards = processAreasForClimate(hierarchy.areas, hass, entities);
204201

205202
if (areaCards.length > 0) {
206203
section.cards!.push(...areaCards);

0 commit comments

Comments
 (0)