diff --git a/src/generators/AreaView.ts b/src/generators/AreaView.ts new file mode 100644 index 0000000..854a621 --- /dev/null +++ b/src/generators/AreaView.ts @@ -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 { + 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; diff --git a/src/generators/domainCardGenerator.ts b/src/generators/domainCardGenerator.ts new file mode 100644 index 0000000..672f6a3 --- /dev/null +++ b/src/generators/domainCardGenerator.ts @@ -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 { + const miscaleaniousCardsPromise = async (): Promise => { + 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 => { + 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(); +} diff --git a/src/mushroom-strategy.ts b/src/mushroom-strategy.ts index d743a56..443b780 100644 --- a/src/mushroom-strategy.ts +++ b/src/mushroom-strategy.ts @@ -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.
@@ -38,11 +29,12 @@ class MushroomStrategy extends HTMLTemplateElement { * Called when opening a dashboard. */ static async generateDashboard(info: DashboardInfo): Promise { - 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 { - 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(); diff --git a/src/types/strategy/strategy-generics.ts b/src/types/strategy/strategy-generics.ts index 7b05ac1..1ad69d5 100644 --- a/src/types/strategy/strategy-generics.ts +++ b/src/types/strategy/strategy-generics.ts @@ -164,6 +164,7 @@ export interface ViewInfo { hass: HomeAssistant; view: LovelaceViewRawConfig & { strategy: { + parentEntry?: AreaRegistryEntry | DeviceRegistryEntry; options?: StrategyConfig & { area: StrategyArea }; }; }; diff --git a/src/types/strategy/type-guards.ts b/src/types/strategy/type-guards.ts new file mode 100644 index 0000000..57478de --- /dev/null +++ b/src/types/strategy/type-guards.ts @@ -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; +}