Refactor Area view generators

- The area views are now generated asynchronously when a specific area
  is opened, rather than all views being generated at once when any
  random area is accessed.
- The logic for generating area views has been separated from the logic
  that handles the overall dashboard.
This commit is contained in:
DigiLive
2025-05-03 19:02:27 +02:00
parent 51dec76cf2
commit 64f838df32
5 changed files with 211 additions and 135 deletions

View File

@@ -0,0 +1,39 @@
import { ViewInfo } from '../types/strategy/strategy-generics';
import { LovelaceViewConfig } from '../types/homeassistant/data/lovelace/config/view';
import RegistryFilter from '../utilities/RegistryFilter';
import { Registry } from '../Registry';
import { generateDomainCards } from './domainCardGenerator';
import { isAreaRegistryEntry } from '../types/strategy/type-guards';
import { logMessage, lvlFatal } from '../utilities/debug';
class AreaView extends HTMLTemplateElement {
/**
* Generate an Area view.
*
* The method creates cards for each domain (e.g., sensors, switches, etc.) in the current area, using a combination
* of Header cards and entity-specific cards.
* It also handles miscellaneous entities that don't fit into any supported domain.
*
* @param {ViewInfo} info The view's strategy information object.
*
* @remarks
* Called upon opening a subview.
*/
static async generateView(info: ViewInfo): Promise<LovelaceViewConfig> {
const parentEntity = info.view.strategy.parentEntry;
if (!isAreaRegistryEntry(parentEntity)) {
logMessage(lvlFatal, `Entity ${parentEntity?.area_id} is not recognized as an area!`);
throw new Error();
}
return {
cards: await generateDomainCards(
Registry.getExposedNames('domain'),
new RegistryFilter(Registry.entities).whereAreaId(parentEntity.area_id).toList(),
),
};
}
}
export default AreaView;

View File

@@ -0,0 +1,118 @@
import { isSupportedDomain } from '../types/strategy/strategy-generics';
import { sanitizeClassName } from '../utilities/auxiliaries';
import RegistryFilter from '../utilities/RegistryFilter';
import { Registry } from '../Registry';
import HeaderCard from '../cards/HeaderCard';
import SensorCard from '../cards/SensorCard';
import { stackHorizontal } from '../utilities/cardStacking';
import { logMessage, lvlError } from '../utilities/debug';
import { EntityRegistryEntry } from '../types/homeassistant/data/entity_registry';
import { LovelaceCardConfig } from '../types/homeassistant/data/lovelace/config/card';
export async function generateDomainCards(
domains: string[],
domainEntities: EntityRegistryEntry[],
): Promise<LovelaceCardConfig[]> {
const miscaleaniousCardsPromise = async (): Promise<LovelaceCardConfig[]> => {
if (Registry.strategyOptions.domains.default.hidden) {
return [];
}
const miscellaneousEntities = new RegistryFilter(domainEntities)
.not()
.where((entity) => isSupportedDomain(entity.entity_id.split('.', 1)[0]))
.toList();
if (!miscellaneousEntities.length) {
return [];
}
try {
const MiscellaneousCard = (await import('../cards/MiscellaneousCard')).default;
const miscellaneousCards = [
new HeaderCard({}, Registry.strategyOptions.domains.default).createCard(),
...miscellaneousEntities.map(
(entity) =>
new MiscellaneousCard(
entity,
Registry.strategyOptions.card_options?.[entity.entity_id],
).getCard() as LovelaceCardConfig,
),
];
return [
{
type: 'vertical-stack',
cards: miscellaneousCards,
},
];
} catch (e) {
logMessage(lvlError, 'Error creating card configurations for domain `miscellaneous`', e);
return [];
}
};
const otherDomainCardPromises = domains
.filter(isSupportedDomain)
.map(async (domain): Promise<LovelaceCardConfig[]> => {
const moduleName = sanitizeClassName(domain + 'Card');
const module = import(`../cards/${moduleName}`);
const entities = new RegistryFilter(domainEntities)
.whereDomain(domain)
.where((entity) => !(domain === 'switch' && entity.entity_id.endsWith('_stateful_scene')))
.toList();
if (!entities.length) {
return [];
}
const titleCard = new HeaderCard(
{ entity_id: entities.map((entity) => entity.entity_id) },
Registry.strategyOptions.domains[domain],
).createCard();
try {
const DomainCard = (await module).default;
if (domain === 'sensor') {
const domainCards = entities
.filter((entity) => Registry.hassStates[entity.entity_id]?.attributes.unit_of_measurement)
.map((entity) => {
const options = {
...(entity.device_id && Registry.strategyOptions.card_options?.[entity.device_id]),
...Registry.strategyOptions.card_options?.[entity.entity_id],
type: 'custom:mini-graph-card',
entities: [entity.entity_id],
};
return new SensorCard(entity, options).getCard();
});
return domainCards.length ? [{ type: 'vertical-stack', cards: [titleCard, ...domainCards] }] : [];
}
let domainCards = entities.map((entity) => {
const cardOptions = {
...(entity.device_id && Registry.strategyOptions.card_options?.[entity.device_id]),
...Registry.strategyOptions.card_options?.[entity.entity_id],
};
return new DomainCard(entity, cardOptions).getCard() as LovelaceCardConfig;
});
if (domain === 'binary_sensor') {
domainCards = stackHorizontal(domainCards);
}
return domainCards.length ? [{ type: 'vertical-stack', cards: [titleCard, ...domainCards] }] : [];
} catch (e) {
logMessage(lvlError, `Error creating card configurations for domain ${domain}`, e);
return [];
}
});
return (await Promise.all([...otherDomainCardPromises, miscaleaniousCardsPromise()])).flat();
}

View File

@@ -1,21 +1,12 @@
import { HassServiceTarget } from 'home-assistant-js-websocket';
import HeaderCard from './cards/HeaderCard';
import SensorCard from './cards/SensorCard';
import { Registry } from './Registry';
import { LovelaceCardConfig } from './types/homeassistant/data/lovelace/config/card';
import { LovelaceConfig } from './types/homeassistant/data/lovelace/config/types';
import { LovelaceViewConfig, LovelaceViewRawConfig } from './types/homeassistant/data/lovelace/config/view';
import {
DashboardInfo,
isSupportedDomain,
isSupportedView,
StrategyArea,
ViewInfo,
} from './types/strategy/strategy-generics';
import { LovelaceViewRawConfig } from './types/homeassistant/data/lovelace/config/view';
import { DashboardInfo, isSupportedView } from './types/strategy/strategy-generics';
import { sanitizeClassName } from './utilities/auxiliaries';
import { logMessage, lvlError } from './utilities/debug';
import { logMessage, lvlError, lvlFatal } from './utilities/debug';
import RegistryFilter from './utilities/RegistryFilter';
import { stackHorizontal } from './utilities/cardStacking';
import DeviceView from './generators/DeviceView';
import AreaView from './generators/AreaView';
/**
* Mushroom Dashboard Strategy.<br>
@@ -38,11 +29,12 @@ class MushroomStrategy extends HTMLTemplateElement {
* Called when opening a dashboard.
*/
static async generateDashboard(info: DashboardInfo): Promise<LovelaceConfig> {
await Registry.initialize(info);
try {
await Registry.initialize(info);
} catch (e) {
logMessage(lvlFatal, 'Error initializing the Registry!', e);
}
const views: LovelaceViewRawConfig[] = [];
// Parallelize view imports and creation.
const viewPromises = Registry.getExposedNames('view')
.filter(isSupportedView)
.map(async (viewName) => {
@@ -62,7 +54,7 @@ class MushroomStrategy extends HTMLTemplateElement {
return null;
});
const resolvedViews = (await Promise.all(viewPromises)).filter(Boolean) as LovelaceViewRawConfig[];
const views = (await Promise.all(viewPromises)).filter(Boolean) as LovelaceViewRawConfig[];
views.push(...resolvedViews);
@@ -73,12 +65,16 @@ class MushroomStrategy extends HTMLTemplateElement {
path: area.area_id,
subview: true,
strategy: {
type: 'custom:mushroom-strategy',
options: { area },
type: 'custom:mushroom-strategy-area-view',
parentEntry: area,
},
})),
);
if (Registry.areas.length) {
customElements.define('ll-strategy-mushroom-strategy-area-view', AreaView);
}
// Extra views
if (Registry.strategyOptions.extra_views) {
views.push(...Registry.strategyOptions.extra_views);
@@ -86,123 +82,22 @@ class MushroomStrategy extends HTMLTemplateElement {
return { views };
}
}
/**
* Generate a view.
*
* The method creates cards for each domain (e.g., sensors, switches, etc.) in the current area, using a combination
* of Header cards and entity-specific cards.
* It also handles miscellaneous entities that don't fit into any supported domain.
*
* @param {ViewInfo} info The view's strategy information object.
*
* @remarks
* Called upon opening a subview.
*/
static async generateView(info: ViewInfo): Promise<LovelaceViewConfig> {
const exposedDomainNames = Registry.getExposedNames('domain');
const area = info.view.strategy?.options?.area ?? ({} as StrategyArea);
const areaEntities = new RegistryFilter(Registry.entities).whereAreaId(area.area_id).toList();
const viewCards: LovelaceCardConfig[] = [...(area.extra_cards ?? [])];
async function main() {
const version = 'v2.3.0-alpha.1';
// Set the target for any Header card to the current area.
const target: HassServiceTarget = { area_id: [area.area_id] };
console.info(
'%c Mushroom Strategy %c '.concat(version, ' '),
'color: white; background: coral; font-weight: 700;',
'color: coral; background: white; font-weight: 700;',
);
// Prepare promises for all supported domains
const domainCardPromises = exposedDomainNames.filter(isSupportedDomain).map(async (domain) => {
const moduleName = sanitizeClassName(domain + 'Card');
const entities = new RegistryFilter(areaEntities)
.whereDomain(domain)
.where((entity) => !(domain === 'switch' && entity.entity_id.endsWith('_stateful_scene')))
.toList();
if (!entities.length) {
return null;
}
const titleCard = new HeaderCard(
{ entity_id: entities.map((entity) => entity.entity_id) },
Registry.strategyOptions.domains[domain],
).createCard();
try {
const DomainCard = (await import(`./cards/${moduleName}`)).default;
if (domain === 'sensor') {
const domainCards = entities
.filter((entity) => Registry.hassStates[entity.entity_id]?.attributes.unit_of_measurement)
.map((entity) => {
const options = {
...(entity.device_id && Registry.strategyOptions.card_options?.[entity.device_id]),
...Registry.strategyOptions.card_options?.[entity.entity_id],
type: 'custom:mini-graph-card',
entities: [entity.entity_id],
};
return new SensorCard(entity, options).getCard();
});
return domainCards.length ? { type: 'vertical-stack', cards: [titleCard, ...domainCards] } : null;
}
let domainCards = entities.map((entity) => {
const cardOptions = {
...(entity.device_id && Registry.strategyOptions.card_options?.[entity.device_id]),
...Registry.strategyOptions.card_options?.[entity.entity_id],
};
return new DomainCard(entity, cardOptions).getCard();
});
if (domain === 'binary_sensor') {
domainCards = stackHorizontal(domainCards);
}
return domainCards.length ? { type: 'vertical-stack', cards: [titleCard, ...domainCards] } : null;
} catch (e) {
logMessage(lvlError, `Error creating card configurations for domain ${domain}`, e);
return null;
}
});
// Await all domain card stacks
const domainCardStacks = (await Promise.all(domainCardPromises)).filter(Boolean) as LovelaceCardConfig[];
viewCards.push(...domainCardStacks);
// Miscellaneous domain
if (!Registry.strategyOptions.domains.default.hidden) {
const miscellaneousEntities = new RegistryFilter(areaEntities)
.not()
.where((entity) => isSupportedDomain(entity.entity_id.split('.', 1)[0]))
.toList();
if (miscellaneousEntities.length) {
try {
const MiscellaneousCard = (await import('./cards/MiscellaneousCard')).default;
const miscellaneousCards = [
new HeaderCard(target, Registry.strategyOptions.domains.default).createCard(),
...miscellaneousEntities.map((entity) =>
new MiscellaneousCard(entity, Registry.strategyOptions.card_options?.[entity.entity_id]).getCard(),
),
];
viewCards.push({
type: 'vertical-stack',
cards: miscellaneousCards,
});
} catch (e) {
logMessage(lvlError, 'Error creating card configurations for domain `miscellaneous`', e);
}
}
}
return { cards: viewCards };
try {
customElements.define('ll-strategy-mushroom-strategy', MushroomStrategy);
} catch (e) {
logMessage(lvlFatal, 'Error defining the Strategy element!', e);
}
}
customElements.define('ll-strategy-mushroom-strategy', MushroomStrategy);
const version = 'v2.3.0-alpha.1';
console.info(
'%c Mushroom Strategy %c '.concat(version, ' '),
'color: white; background: coral; font-weight: 700;',
'color: coral; background: white; font-weight: 700;',
);
main();

View File

@@ -164,6 +164,7 @@ export interface ViewInfo {
hass: HomeAssistant;
view: LovelaceViewRawConfig & {
strategy: {
parentEntry?: AreaRegistryEntry | DeviceRegistryEntry;
options?: StrategyConfig & { area: StrategyArea };
};
};

View File

@@ -0,0 +1,23 @@
import { DeviceRegistryEntry } from '../homeassistant/data/device_registry';
import { RegistryEntry } from './strategy-generics';
import { AreaRegistryEntry } from '../homeassistant/data/area_registry';
/**
* Type guard to check if the given object is a DeviceRegistryEntry.
*
* @param [object] - The object to check.
* @returns True if the object is a DeviceRegistryEntry, false otherwise.
*/
export function isDeviceRegistryEntry(object?: RegistryEntry): object is DeviceRegistryEntry {
return !!object && 'id' in object && 'model' in object;
}
/**
* Type guard to check if the given object is an AreaRegistryEntry.
*
* @param [object] - The object to check.
* @returns True if the object is a AreaRegistryEntry, false otherwise.
*/
export function isAreaRegistryEntry(object?: RegistryEntry): object is AreaRegistryEntry {
return !!object && 'area_id' in object;
}