mirror of
https://github.com/DigiLive/mushroom-strategy.git
synced 2025-08-03 19:44:27 +02:00
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:
39
src/generators/AreaView.ts
Normal file
39
src/generators/AreaView.ts
Normal 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;
|
118
src/generators/domainCardGenerator.ts
Normal file
118
src/generators/domainCardGenerator.ts
Normal 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();
|
||||
}
|
@@ -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();
|
||||
|
@@ -164,6 +164,7 @@ export interface ViewInfo {
|
||||
hass: HomeAssistant;
|
||||
view: LovelaceViewRawConfig & {
|
||||
strategy: {
|
||||
parentEntry?: AreaRegistryEntry | DeviceRegistryEntry;
|
||||
options?: StrategyConfig & { area: StrategyArea };
|
||||
};
|
||||
};
|
||||
|
23
src/types/strategy/type-guards.ts
Normal file
23
src/types/strategy/type-guards.ts
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user