mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-18 23:06:40 +00:00
Add grouping by device to generated dashboard config (#14398)
This commit is contained in:
parent
4846fa1a74
commit
c119163422
@ -88,7 +88,7 @@ class HcCast extends LitElement {
|
||||
>
|
||||
${(this.lovelaceConfig
|
||||
? this.lovelaceConfig.views
|
||||
: [generateDefaultViewConfig([], [], [], {}, () => "")]
|
||||
: [generateDefaultViewConfig({}, {}, {}, {}, () => "")]
|
||||
).map(
|
||||
(view, idx) => html`
|
||||
<paper-icon-item
|
||||
|
@ -6,17 +6,16 @@ import { splitByGroups } from "../../../common/entity/split_by_groups";
|
||||
import { stripPrefixFromEntityName } from "../../../common/entity/strip_prefix_from_entity_name";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||
import {
|
||||
EnergyPreferences,
|
||||
GridSourceTypeEnergyPreference,
|
||||
} from "../../../data/energy";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import { LovelaceCardConfig, LovelaceViewConfig } from "../../../data/lovelace";
|
||||
import { SENSOR_DEVICE_CLASS_BATTERY } from "../../../data/sensor";
|
||||
import { computeUserInitials } from "../../../data/user";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { HELPER_DOMAINS } from "../../config/helpers/const";
|
||||
import {
|
||||
AlarmPanelCardConfig,
|
||||
EntitiesCardConfig,
|
||||
@ -26,7 +25,6 @@ import {
|
||||
} from "../cards/types";
|
||||
import { LovelaceRowConfig } from "../entity-rows/types";
|
||||
import { ButtonsHeaderFooterConfig } from "../header-footer/types";
|
||||
import { HELPER_DOMAINS } from "../../config/helpers/const";
|
||||
|
||||
const HIDE_DOMAIN = new Set([
|
||||
"automation",
|
||||
@ -41,47 +39,58 @@ const HIDE_DOMAIN = new Set([
|
||||
|
||||
const HIDE_PLATFORM = new Set(["mobile_app"]);
|
||||
|
||||
interface SplittedByAreas {
|
||||
areasWithEntities: Array<[AreaRegistryEntry, HassEntity[]]>;
|
||||
interface SplittedByAreaDevice {
|
||||
areasWithEntities: { [areaId: string]: HassEntity[] };
|
||||
devicesWithEntities: { [deviceId: string]: HassEntity[] };
|
||||
otherEntities: HassEntities;
|
||||
}
|
||||
|
||||
const splitByAreas = (
|
||||
areaEntries: AreaRegistryEntry[],
|
||||
deviceEntries: DeviceRegistryEntry[],
|
||||
entityEntries: EntityRegistryEntry[],
|
||||
const splitByAreaDevice = (
|
||||
deviceEntries: HomeAssistant["devices"],
|
||||
entityEntries: HomeAssistant["entities"],
|
||||
entities: HassEntities
|
||||
): SplittedByAreas => {
|
||||
): SplittedByAreaDevice => {
|
||||
const allEntities = { ...entities };
|
||||
const areasWithEntities: SplittedByAreas["areasWithEntities"] = [];
|
||||
const areasWithEntities: SplittedByAreaDevice["areasWithEntities"] = {};
|
||||
const devicesWithEntities: SplittedByAreaDevice["devicesWithEntities"] = {};
|
||||
|
||||
for (const area of areaEntries) {
|
||||
const areaEntities: HassEntity[] = [];
|
||||
const areaDevices = new Set(
|
||||
deviceEntries
|
||||
.filter((device) => device.area_id === area.area_id)
|
||||
Object.values(deviceEntries)
|
||||
.filter((device) => device.area_id)
|
||||
.map((device) => device.id)
|
||||
);
|
||||
for (const entity of entityEntries) {
|
||||
for (const entity of Object.values(entityEntries)) {
|
||||
if (
|
||||
((areaDevices.has(
|
||||
// @ts-ignore
|
||||
entity.device_id
|
||||
) &&
|
||||
!entity.area_id) ||
|
||||
entity.area_id === area.area_id) &&
|
||||
(entity.area_id ||
|
||||
(entity.device_id && areaDevices.has(entity.device_id))) &&
|
||||
entity.entity_id in allEntities
|
||||
) {
|
||||
areaEntities.push(allEntities[entity.entity_id]);
|
||||
const areaId =
|
||||
entity.area_id || deviceEntries[entity.device_id!].area_id!;
|
||||
if (!(areaId in areasWithEntities)) {
|
||||
areasWithEntities[areaId] = [];
|
||||
}
|
||||
areasWithEntities[areaId].push(allEntities[entity.entity_id]);
|
||||
delete allEntities[entity.entity_id];
|
||||
} else if (entity.device_id && entity.entity_id in allEntities) {
|
||||
if (!(entity.device_id in devicesWithEntities)) {
|
||||
devicesWithEntities[entity.device_id] = [];
|
||||
}
|
||||
devicesWithEntities[entity.device_id].push(allEntities[entity.entity_id]);
|
||||
delete allEntities[entity.entity_id];
|
||||
}
|
||||
}
|
||||
if (areaEntities.length > 0) {
|
||||
areasWithEntities.push([area, areaEntities]);
|
||||
for (const [deviceId, deviceEntities] of Object.entries(
|
||||
devicesWithEntities
|
||||
)) {
|
||||
if (deviceEntities.length === 1) {
|
||||
allEntities[deviceEntities[0].entity_id] = deviceEntities[0];
|
||||
delete devicesWithEntities[deviceId];
|
||||
}
|
||||
}
|
||||
return {
|
||||
areasWithEntities,
|
||||
devicesWithEntities,
|
||||
otherEntities: allEntities,
|
||||
};
|
||||
};
|
||||
@ -232,11 +241,11 @@ export const computeCards = (
|
||||
|
||||
const computeDefaultViewStates = (
|
||||
entities: HassEntities,
|
||||
entityEntries: EntityRegistryEntry[]
|
||||
entityEntries: HomeAssistant["entities"]
|
||||
): HassEntities => {
|
||||
const states = {};
|
||||
const hiddenEntities = new Set(
|
||||
entityEntries
|
||||
Object.values(entityEntries)
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.entity_category ||
|
||||
@ -246,7 +255,7 @@ const computeDefaultViewStates = (
|
||||
.map((entry) => entry.entity_id)
|
||||
);
|
||||
|
||||
Object.keys(entities).forEach((entityId) => {
|
||||
for (const entityId of Object.keys(entities)) {
|
||||
const stateObj = entities[entityId];
|
||||
if (
|
||||
!HIDE_DOMAIN.has(computeStateDomain(stateObj)) &&
|
||||
@ -254,7 +263,7 @@ const computeDefaultViewStates = (
|
||||
) {
|
||||
states[entityId] = entities[entityId];
|
||||
}
|
||||
});
|
||||
}
|
||||
return states;
|
||||
};
|
||||
|
||||
@ -274,7 +283,7 @@ export const generateViewConfig = (
|
||||
const ungroupedEntitites: { [domain: string]: string[] } = {};
|
||||
|
||||
// Organize ungrouped entities in ungrouped things
|
||||
Object.keys(splitted.ungrouped).forEach((entityId) => {
|
||||
for (const entityId of Object.keys(splitted.ungrouped)) {
|
||||
const state = splitted.ungrouped[entityId];
|
||||
const domain = computeStateDomain(state);
|
||||
|
||||
@ -283,7 +292,7 @@ export const generateViewConfig = (
|
||||
}
|
||||
|
||||
ungroupedEntitites[domain].push(state.entity_id);
|
||||
});
|
||||
}
|
||||
|
||||
const cards: LovelaceCardConfig[] = [];
|
||||
|
||||
@ -343,7 +352,7 @@ export const generateViewConfig = (
|
||||
delete ungroupedEntitites.person;
|
||||
}
|
||||
|
||||
splitted.groups.forEach((groupEntity) => {
|
||||
for (const groupEntity of splitted.groups) {
|
||||
cards.push(
|
||||
...computeCards(
|
||||
groupEntity.attributes.entity_id.map(
|
||||
@ -355,7 +364,7 @@ export const generateViewConfig = (
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Group helper entities in a single card
|
||||
const helperEntities: string[] = [];
|
||||
@ -421,9 +430,9 @@ export const generateViewConfig = (
|
||||
};
|
||||
|
||||
export const generateDefaultViewConfig = (
|
||||
areaEntries: AreaRegistryEntry[],
|
||||
deviceEntries: DeviceRegistryEntry[],
|
||||
entityEntries: EntityRegistryEntry[],
|
||||
areaEntries: HomeAssistant["areas"],
|
||||
deviceEntries: HomeAssistant["devices"],
|
||||
entityEntries: HomeAssistant["entities"],
|
||||
entities: HassEntities,
|
||||
localize: LocalizeFunc,
|
||||
energyPrefs?: EnergyPreferences
|
||||
@ -435,15 +444,14 @@ export const generateDefaultViewConfig = (
|
||||
|
||||
// In the case of a default view, we want to use the group order attribute
|
||||
const groupOrders = {};
|
||||
Object.keys(states).forEach((entityId) => {
|
||||
for (const entityId of Object.keys(states)) {
|
||||
const stateObj = states[entityId];
|
||||
if (stateObj.attributes.order) {
|
||||
groupOrders[entityId] = stateObj.attributes.order;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const splittedByAreas = splitByAreas(
|
||||
areaEntries,
|
||||
const splittedByAreaDevice = splitByAreaDevice(
|
||||
deviceEntries,
|
||||
entityEntries,
|
||||
states
|
||||
@ -454,14 +462,17 @@ export const generateDefaultViewConfig = (
|
||||
path,
|
||||
title,
|
||||
icon,
|
||||
splittedByAreas.otherEntities,
|
||||
splittedByAreaDevice.otherEntities,
|
||||
groupOrders
|
||||
);
|
||||
|
||||
const areaCards: LovelaceCardConfig[] = [];
|
||||
const splittedCards: LovelaceCardConfig[] = [];
|
||||
|
||||
splittedByAreas.areasWithEntities.forEach(([area, areaEntities]) => {
|
||||
areaCards.push(
|
||||
for (const [areaId, areaEntities] of Object.entries(
|
||||
splittedByAreaDevice.areasWithEntities
|
||||
)) {
|
||||
const area = areaEntries[areaId];
|
||||
splittedCards.push(
|
||||
...computeCards(
|
||||
areaEntities.map((entity) => [entity.entity_id, entity]),
|
||||
{
|
||||
@ -469,8 +480,29 @@ export const generateDefaultViewConfig = (
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
}
|
||||
for (const [deviceId, deviceEntities] of Object.entries(
|
||||
splittedByAreaDevice.devicesWithEntities
|
||||
)) {
|
||||
const device = deviceEntries[deviceId];
|
||||
splittedCards.push(
|
||||
...computeCards(
|
||||
deviceEntities.map((entity) => [entity.entity_id, entity]),
|
||||
{
|
||||
title:
|
||||
device.name_by_user ||
|
||||
device.name ||
|
||||
localize(
|
||||
"ui.panel.config.devices.unnamed_device",
|
||||
"type",
|
||||
localize(
|
||||
`ui.panel.config.devices.type.${device.entry_type || "device"}`
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
if (energyPrefs) {
|
||||
// Distribution card requires the grid to be configured
|
||||
const grid = energyPrefs.energy_sources.find(
|
||||
@ -478,7 +510,7 @@ export const generateDefaultViewConfig = (
|
||||
) as GridSourceTypeEnergyPreference | undefined;
|
||||
|
||||
if (grid && grid.flow_from.length > 0) {
|
||||
areaCards.push({
|
||||
splittedCards.push({
|
||||
title: localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.title_today"
|
||||
),
|
||||
@ -488,7 +520,7 @@ export const generateDefaultViewConfig = (
|
||||
}
|
||||
}
|
||||
|
||||
config.cards!.unshift(...areaCards);
|
||||
config.cards!.unshift(...splittedCards);
|
||||
|
||||
return config;
|
||||
};
|
||||
|
@ -1,18 +1,12 @@
|
||||
import { STATE_NOT_RUNNING } from "home-assistant-js-websocket";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { subscribeOne } from "../../../common/util/subscribe-one";
|
||||
import { subscribeAreaRegistry } from "../../../data/area_registry";
|
||||
import { subscribeDeviceRegistry } from "../../../data/device_registry";
|
||||
import { getEnergyPreferences } from "../../../data/energy";
|
||||
import { subscribeEntityRegistry } from "../../../data/entity_registry";
|
||||
import { generateDefaultViewConfig } from "../common/generate-lovelace-config";
|
||||
import {
|
||||
LovelaceDashboardStrategy,
|
||||
LovelaceViewStrategy,
|
||||
} from "./get-strategy";
|
||||
|
||||
let subscribedRegistries = false;
|
||||
|
||||
export class OriginalStatesStrategy {
|
||||
static async generateView(
|
||||
info: Parameters<LovelaceViewStrategy["generateView"]>[0]
|
||||
@ -31,19 +25,7 @@ export class OriginalStatesStrategy {
|
||||
};
|
||||
}
|
||||
|
||||
// We leave this here so we always have the freshest data.
|
||||
if (!subscribedRegistries) {
|
||||
subscribedRegistries = true;
|
||||
subscribeAreaRegistry(hass.connection, () => undefined);
|
||||
subscribeDeviceRegistry(hass.connection, () => undefined);
|
||||
subscribeEntityRegistry(hass.connection, () => undefined);
|
||||
}
|
||||
|
||||
const [areaEntries, deviceEntries, entityEntries, localize, energyPrefs] =
|
||||
await Promise.all([
|
||||
subscribeOne(hass.connection, subscribeAreaRegistry),
|
||||
subscribeOne(hass.connection, subscribeDeviceRegistry),
|
||||
subscribeOne(hass.connection, subscribeEntityRegistry),
|
||||
const [localize, energyPrefs] = await Promise.all([
|
||||
hass.loadBackendTranslation("title"),
|
||||
isComponentLoaded(hass, "energy")
|
||||
? // It raises if not configured, just swallow that.
|
||||
@ -54,9 +36,9 @@ export class OriginalStatesStrategy {
|
||||
// User can override default view. If they didn't, we will add one
|
||||
// that contains all entities.
|
||||
const view = generateDefaultViewConfig(
|
||||
areaEntries,
|
||||
deviceEntries,
|
||||
entityEntries,
|
||||
hass.areas,
|
||||
hass.devices,
|
||||
hass.entities,
|
||||
hass.states,
|
||||
localize,
|
||||
energyPrefs
|
||||
|
Loading…
x
Reference in New Issue
Block a user