Add grouping by device to generated dashboard config (#14398)

This commit is contained in:
Bram Kragten 2022-11-28 16:56:35 +01:00 committed by GitHub
parent 4846fa1a74
commit c119163422
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 99 additions and 85 deletions

View File

@ -88,7 +88,7 @@ class HcCast extends LitElement {
>
${(this.lovelaceConfig
? this.lovelaceConfig.views
: [generateDefaultViewConfig([], [], [], {}, () => "")]
: [generateDefaultViewConfig({}, {}, {}, {}, () => "")]
).map(
(view, idx) => html`
<paper-icon-item

View File

@ -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;
};

View File

@ -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