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
|
||||||
? this.lovelaceConfig.views
|
? this.lovelaceConfig.views
|
||||||
: [generateDefaultViewConfig([], [], [], {}, () => "")]
|
: [generateDefaultViewConfig({}, {}, {}, {}, () => "")]
|
||||||
).map(
|
).map(
|
||||||
(view, idx) => html`
|
(view, idx) => html`
|
||||||
<paper-icon-item
|
<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 { 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 areaDevices = new Set(
|
||||||
const areaEntities: HassEntity[] = [];
|
Object.values(deviceEntries)
|
||||||
const areaDevices = new Set(
|
.filter((device) => device.area_id)
|
||||||
deviceEntries
|
.map((device) => device.id)
|
||||||
.filter((device) => device.area_id === area.area_id)
|
);
|
||||||
.map((device) => device.id)
|
for (const entity of Object.values(entityEntries)) {
|
||||||
);
|
if (
|
||||||
for (const entity of entityEntries) {
|
(entity.area_id ||
|
||||||
if (
|
(entity.device_id && areaDevices.has(entity.device_id))) &&
|
||||||
((areaDevices.has(
|
entity.entity_id in allEntities
|
||||||
// @ts-ignore
|
) {
|
||||||
entity.device_id
|
const areaId =
|
||||||
) &&
|
entity.area_id || deviceEntries[entity.device_id!].area_id!;
|
||||||
!entity.area_id) ||
|
if (!(areaId in areasWithEntities)) {
|
||||||
entity.area_id === area.area_id) &&
|
areasWithEntities[areaId] = [];
|
||||||
entity.entity_id in allEntities
|
|
||||||
) {
|
|
||||||
areaEntities.push(allEntities[entity.entity_id]);
|
|
||||||
delete allEntities[entity.entity_id];
|
|
||||||
}
|
}
|
||||||
|
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 {
|
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;
|
||||||
};
|
};
|
||||||
|
@ -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,32 +25,20 @@ export class OriginalStatesStrategy {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// We leave this here so we always have the freshest data.
|
const [localize, energyPrefs] = await Promise.all([
|
||||||
if (!subscribedRegistries) {
|
hass.loadBackendTranslation("title"),
|
||||||
subscribedRegistries = true;
|
isComponentLoaded(hass, "energy")
|
||||||
subscribeAreaRegistry(hass.connection, () => undefined);
|
? // It raises if not configured, just swallow that.
|
||||||
subscribeDeviceRegistry(hass.connection, () => undefined);
|
getEnergyPreferences(hass).catch(() => undefined)
|
||||||
subscribeEntityRegistry(hass.connection, () => undefined);
|
: 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"),
|
|
||||||
isComponentLoaded(hass, "energy")
|
|
||||||
? // It raises if not configured, just swallow that.
|
|
||||||
getEnergyPreferences(hass).catch(() => undefined)
|
|
||||||
: undefined,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user