mirror of
https://github.com/DigiLive/mushroom-strategy.git
synced 2025-08-04 03:54: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 { Registry } from './Registry';
|
||||||
import { LovelaceCardConfig } from './types/homeassistant/data/lovelace/config/card';
|
|
||||||
import { LovelaceConfig } from './types/homeassistant/data/lovelace/config/types';
|
import { LovelaceConfig } from './types/homeassistant/data/lovelace/config/types';
|
||||||
import { LovelaceViewConfig, LovelaceViewRawConfig } from './types/homeassistant/data/lovelace/config/view';
|
import { LovelaceViewRawConfig } from './types/homeassistant/data/lovelace/config/view';
|
||||||
import {
|
import { DashboardInfo, isSupportedView } from './types/strategy/strategy-generics';
|
||||||
DashboardInfo,
|
|
||||||
isSupportedDomain,
|
|
||||||
isSupportedView,
|
|
||||||
StrategyArea,
|
|
||||||
ViewInfo,
|
|
||||||
} from './types/strategy/strategy-generics';
|
|
||||||
import { sanitizeClassName } from './utilities/auxiliaries';
|
import { sanitizeClassName } from './utilities/auxiliaries';
|
||||||
import { logMessage, lvlError } from './utilities/debug';
|
import { logMessage, lvlError, lvlFatal } from './utilities/debug';
|
||||||
import RegistryFilter from './utilities/RegistryFilter';
|
import RegistryFilter from './utilities/RegistryFilter';
|
||||||
import { stackHorizontal } from './utilities/cardStacking';
|
import DeviceView from './generators/DeviceView';
|
||||||
|
import AreaView from './generators/AreaView';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mushroom Dashboard Strategy.<br>
|
* Mushroom Dashboard Strategy.<br>
|
||||||
@@ -38,11 +29,12 @@ class MushroomStrategy extends HTMLTemplateElement {
|
|||||||
* Called when opening a dashboard.
|
* Called when opening a dashboard.
|
||||||
*/
|
*/
|
||||||
static async generateDashboard(info: DashboardInfo): Promise<LovelaceConfig> {
|
static async generateDashboard(info: DashboardInfo): Promise<LovelaceConfig> {
|
||||||
|
try {
|
||||||
await Registry.initialize(info);
|
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')
|
const viewPromises = Registry.getExposedNames('view')
|
||||||
.filter(isSupportedView)
|
.filter(isSupportedView)
|
||||||
.map(async (viewName) => {
|
.map(async (viewName) => {
|
||||||
@@ -62,7 +54,7 @@ class MushroomStrategy extends HTMLTemplateElement {
|
|||||||
return null;
|
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);
|
views.push(...resolvedViews);
|
||||||
|
|
||||||
@@ -73,12 +65,16 @@ class MushroomStrategy extends HTMLTemplateElement {
|
|||||||
path: area.area_id,
|
path: area.area_id,
|
||||||
subview: true,
|
subview: true,
|
||||||
strategy: {
|
strategy: {
|
||||||
type: 'custom:mushroom-strategy',
|
type: 'custom:mushroom-strategy-area-view',
|
||||||
options: { area },
|
parentEntry: area,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (Registry.areas.length) {
|
||||||
|
customElements.define('ll-strategy-mushroom-strategy-area-view', AreaView);
|
||||||
|
}
|
||||||
|
|
||||||
// Extra views
|
// Extra views
|
||||||
if (Registry.strategyOptions.extra_views) {
|
if (Registry.strategyOptions.extra_views) {
|
||||||
views.push(...Registry.strategyOptions.extra_views);
|
views.push(...Registry.strategyOptions.extra_views);
|
||||||
@@ -86,123 +82,22 @@ class MushroomStrategy extends HTMLTemplateElement {
|
|||||||
|
|
||||||
return { views };
|
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 ?? [])];
|
|
||||||
|
|
||||||
// Set the target for any Header card to the current area.
|
|
||||||
const target: HassServiceTarget = { area_id: [area.area_id] };
|
|
||||||
|
|
||||||
// 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 };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('ll-strategy-mushroom-strategy', MushroomStrategy);
|
async function main() {
|
||||||
|
const version = 'v2.3.0-alpha.1';
|
||||||
|
|
||||||
const version = 'v2.3.0-alpha.1';
|
console.info(
|
||||||
console.info(
|
|
||||||
'%c Mushroom Strategy %c '.concat(version, ' '),
|
'%c Mushroom Strategy %c '.concat(version, ' '),
|
||||||
'color: white; background: coral; font-weight: 700;',
|
'color: white; background: coral; font-weight: 700;',
|
||||||
'color: coral; background: white; font-weight: 700;',
|
'color: coral; background: white; font-weight: 700;',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
customElements.define('ll-strategy-mushroom-strategy', MushroomStrategy);
|
||||||
|
} catch (e) {
|
||||||
|
logMessage(lvlFatal, 'Error defining the Strategy element!', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
@@ -164,6 +164,7 @@ export interface ViewInfo {
|
|||||||
hass: HomeAssistant;
|
hass: HomeAssistant;
|
||||||
view: LovelaceViewRawConfig & {
|
view: LovelaceViewRawConfig & {
|
||||||
strategy: {
|
strategy: {
|
||||||
|
parentEntry?: AreaRegistryEntry | DeviceRegistryEntry;
|
||||||
options?: StrategyConfig & { area: StrategyArea };
|
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