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
? this.lovelaceConfig.views ? this.lovelaceConfig.views
: [generateDefaultViewConfig([], [], [], {}, () => "")] : [generateDefaultViewConfig({}, {}, {}, {}, () => "")]
).map( ).map(
(view, idx) => html` (view, idx) => html`
<paper-icon-item <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 { stripPrefixFromEntityName } from "../../../common/entity/strip_prefix_from_entity_name";
import { stringCompare } from "../../../common/string/compare"; import { stringCompare } from "../../../common/string/compare";
import { LocalizeFunc } from "../../../common/translations/localize"; import { LocalizeFunc } from "../../../common/translations/localize";
import type { AreaRegistryEntry } from "../../../data/area_registry";
import type { DeviceRegistryEntry } from "../../../data/device_registry";
import { import {
EnergyPreferences, EnergyPreferences,
GridSourceTypeEnergyPreference, GridSourceTypeEnergyPreference,
} from "../../../data/energy"; } from "../../../data/energy";
import type { EntityRegistryEntry } from "../../../data/entity_registry";
import { domainToName } from "../../../data/integration"; import { domainToName } from "../../../data/integration";
import { LovelaceCardConfig, LovelaceViewConfig } from "../../../data/lovelace"; import { LovelaceCardConfig, LovelaceViewConfig } from "../../../data/lovelace";
import { SENSOR_DEVICE_CLASS_BATTERY } from "../../../data/sensor"; import { SENSOR_DEVICE_CLASS_BATTERY } from "../../../data/sensor";
import { computeUserInitials } from "../../../data/user"; import { computeUserInitials } from "../../../data/user";
import { HomeAssistant } from "../../../types";
import { HELPER_DOMAINS } from "../../config/helpers/const";
import { import {
AlarmPanelCardConfig, AlarmPanelCardConfig,
EntitiesCardConfig, EntitiesCardConfig,
@ -26,7 +25,6 @@ import {
} from "../cards/types"; } from "../cards/types";
import { LovelaceRowConfig } from "../entity-rows/types"; import { LovelaceRowConfig } from "../entity-rows/types";
import { ButtonsHeaderFooterConfig } from "../header-footer/types"; import { ButtonsHeaderFooterConfig } from "../header-footer/types";
import { HELPER_DOMAINS } from "../../config/helpers/const";
const HIDE_DOMAIN = new Set([ const HIDE_DOMAIN = new Set([
"automation", "automation",
@ -41,47 +39,58 @@ const HIDE_DOMAIN = new Set([
const HIDE_PLATFORM = new Set(["mobile_app"]); const HIDE_PLATFORM = new Set(["mobile_app"]);
interface SplittedByAreas { interface SplittedByAreaDevice {
areasWithEntities: Array<[AreaRegistryEntry, HassEntity[]]>; areasWithEntities: { [areaId: string]: HassEntity[] };
devicesWithEntities: { [deviceId: string]: HassEntity[] };
otherEntities: HassEntities; otherEntities: HassEntities;
} }
const splitByAreas = ( const splitByAreaDevice = (
areaEntries: AreaRegistryEntry[], deviceEntries: HomeAssistant["devices"],
deviceEntries: DeviceRegistryEntry[], entityEntries: HomeAssistant["entities"],
entityEntries: EntityRegistryEntry[],
entities: HassEntities entities: HassEntities
): SplittedByAreas => { ): SplittedByAreaDevice => {
const allEntities = { ...entities }; 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( const areaDevices = new Set(
deviceEntries Object.values(deviceEntries)
.filter((device) => device.area_id === area.area_id) .filter((device) => device.area_id)
.map((device) => device.id) .map((device) => device.id)
); );
for (const entity of entityEntries) { for (const entity of Object.values(entityEntries)) {
if ( if (
((areaDevices.has( (entity.area_id ||
// @ts-ignore (entity.device_id && areaDevices.has(entity.device_id))) &&
entity.device_id
) &&
!entity.area_id) ||
entity.area_id === area.area_id) &&
entity.entity_id in allEntities 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]; delete allEntities[entity.entity_id];
} }
} }
if (areaEntities.length > 0) { for (const [deviceId, deviceEntities] of Object.entries(
areasWithEntities.push([area, areaEntities]); devicesWithEntities
)) {
if (deviceEntities.length === 1) {
allEntities[deviceEntities[0].entity_id] = deviceEntities[0];
delete devicesWithEntities[deviceId];
} }
} }
return { return {
areasWithEntities, areasWithEntities,
devicesWithEntities,
otherEntities: allEntities, otherEntities: allEntities,
}; };
}; };
@ -232,11 +241,11 @@ export const computeCards = (
const computeDefaultViewStates = ( const computeDefaultViewStates = (
entities: HassEntities, entities: HassEntities,
entityEntries: EntityRegistryEntry[] entityEntries: HomeAssistant["entities"]
): HassEntities => { ): HassEntities => {
const states = {}; const states = {};
const hiddenEntities = new Set( const hiddenEntities = new Set(
entityEntries Object.values(entityEntries)
.filter( .filter(
(entry) => (entry) =>
entry.entity_category || entry.entity_category ||
@ -246,7 +255,7 @@ const computeDefaultViewStates = (
.map((entry) => entry.entity_id) .map((entry) => entry.entity_id)
); );
Object.keys(entities).forEach((entityId) => { for (const entityId of Object.keys(entities)) {
const stateObj = entities[entityId]; const stateObj = entities[entityId];
if ( if (
!HIDE_DOMAIN.has(computeStateDomain(stateObj)) && !HIDE_DOMAIN.has(computeStateDomain(stateObj)) &&
@ -254,7 +263,7 @@ const computeDefaultViewStates = (
) { ) {
states[entityId] = entities[entityId]; states[entityId] = entities[entityId];
} }
}); }
return states; return states;
}; };
@ -274,7 +283,7 @@ export const generateViewConfig = (
const ungroupedEntitites: { [domain: string]: string[] } = {}; const ungroupedEntitites: { [domain: string]: string[] } = {};
// Organize ungrouped entities in ungrouped things // 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 state = splitted.ungrouped[entityId];
const domain = computeStateDomain(state); const domain = computeStateDomain(state);
@ -283,7 +292,7 @@ export const generateViewConfig = (
} }
ungroupedEntitites[domain].push(state.entity_id); ungroupedEntitites[domain].push(state.entity_id);
}); }
const cards: LovelaceCardConfig[] = []; const cards: LovelaceCardConfig[] = [];
@ -343,7 +352,7 @@ export const generateViewConfig = (
delete ungroupedEntitites.person; delete ungroupedEntitites.person;
} }
splitted.groups.forEach((groupEntity) => { for (const groupEntity of splitted.groups) {
cards.push( cards.push(
...computeCards( ...computeCards(
groupEntity.attributes.entity_id.map( groupEntity.attributes.entity_id.map(
@ -355,7 +364,7 @@ export const generateViewConfig = (
} }
) )
); );
}); }
// Group helper entities in a single card // Group helper entities in a single card
const helperEntities: string[] = []; const helperEntities: string[] = [];
@ -421,9 +430,9 @@ export const generateViewConfig = (
}; };
export const generateDefaultViewConfig = ( export const generateDefaultViewConfig = (
areaEntries: AreaRegistryEntry[], areaEntries: HomeAssistant["areas"],
deviceEntries: DeviceRegistryEntry[], deviceEntries: HomeAssistant["devices"],
entityEntries: EntityRegistryEntry[], entityEntries: HomeAssistant["entities"],
entities: HassEntities, entities: HassEntities,
localize: LocalizeFunc, localize: LocalizeFunc,
energyPrefs?: EnergyPreferences energyPrefs?: EnergyPreferences
@ -435,15 +444,14 @@ export const generateDefaultViewConfig = (
// In the case of a default view, we want to use the group order attribute // In the case of a default view, we want to use the group order attribute
const groupOrders = {}; const groupOrders = {};
Object.keys(states).forEach((entityId) => { for (const entityId of Object.keys(states)) {
const stateObj = states[entityId]; const stateObj = states[entityId];
if (stateObj.attributes.order) { if (stateObj.attributes.order) {
groupOrders[entityId] = stateObj.attributes.order; groupOrders[entityId] = stateObj.attributes.order;
} }
}); }
const splittedByAreas = splitByAreas( const splittedByAreaDevice = splitByAreaDevice(
areaEntries,
deviceEntries, deviceEntries,
entityEntries, entityEntries,
states states
@ -454,14 +462,17 @@ export const generateDefaultViewConfig = (
path, path,
title, title,
icon, icon,
splittedByAreas.otherEntities, splittedByAreaDevice.otherEntities,
groupOrders groupOrders
); );
const areaCards: LovelaceCardConfig[] = []; const splittedCards: LovelaceCardConfig[] = [];
splittedByAreas.areasWithEntities.forEach(([area, areaEntities]) => { for (const [areaId, areaEntities] of Object.entries(
areaCards.push( splittedByAreaDevice.areasWithEntities
)) {
const area = areaEntries[areaId];
splittedCards.push(
...computeCards( ...computeCards(
areaEntities.map((entity) => [entity.entity_id, entity]), 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) { if (energyPrefs) {
// Distribution card requires the grid to be configured // Distribution card requires the grid to be configured
const grid = energyPrefs.energy_sources.find( const grid = energyPrefs.energy_sources.find(
@ -478,7 +510,7 @@ export const generateDefaultViewConfig = (
) as GridSourceTypeEnergyPreference | undefined; ) as GridSourceTypeEnergyPreference | undefined;
if (grid && grid.flow_from.length > 0) { if (grid && grid.flow_from.length > 0) {
areaCards.push({ splittedCards.push({
title: localize( title: localize(
"ui.panel.lovelace.cards.energy.energy_distribution.title_today" "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; return config;
}; };

View File

@ -1,18 +1,12 @@
import { STATE_NOT_RUNNING } from "home-assistant-js-websocket"; import { STATE_NOT_RUNNING } from "home-assistant-js-websocket";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; 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 { getEnergyPreferences } from "../../../data/energy";
import { subscribeEntityRegistry } from "../../../data/entity_registry";
import { generateDefaultViewConfig } from "../common/generate-lovelace-config"; import { generateDefaultViewConfig } from "../common/generate-lovelace-config";
import { import {
LovelaceDashboardStrategy, LovelaceDashboardStrategy,
LovelaceViewStrategy, LovelaceViewStrategy,
} from "./get-strategy"; } from "./get-strategy";
let subscribedRegistries = false;
export class OriginalStatesStrategy { export class OriginalStatesStrategy {
static async generateView( static async generateView(
info: Parameters<LovelaceViewStrategy["generateView"]>[0] info: Parameters<LovelaceViewStrategy["generateView"]>[0]
@ -31,19 +25,7 @@ export class OriginalStatesStrategy {
}; };
} }
// We leave this here so we always have the freshest data. const [localize, energyPrefs] = await Promise.all([
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),
hass.loadBackendTranslation("title"), hass.loadBackendTranslation("title"),
isComponentLoaded(hass, "energy") isComponentLoaded(hass, "energy")
? // It raises if not configured, just swallow that. ? // 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 // User can override default view. If they didn't, we will add one
// that contains all entities. // that contains all entities.
const view = generateDefaultViewConfig( const view = generateDefaultViewConfig(
areaEntries, hass.areas,
deviceEntries, hass.devices,
entityEntries, hass.entities,
hass.states, hass.states,
localize, localize,
energyPrefs energyPrefs