diff --git a/src/mushroom-strategy.ts b/src/mushroom-strategy.ts index 8b7ae7a..b90bd9b 100644 --- a/src/mushroom-strategy.ts +++ b/src/mushroom-strategy.ts @@ -1,18 +1,20 @@ -import {Helper} from "./Helper"; -import {SensorCard} from "./cards/SensorCard"; -import {ControllerCard} from "./cards/ControllerCard"; -import {EntityCardConfig} from "./types/lovelace-mushroom/cards/entity-card-config"; -import {HassServiceTarget} from "home-assistant-js-websocket"; -import {applyEntityCategoryFilters} from "./utillties/filters"; -import {LovelaceConfig} from "./types/homeassistant/data/lovelace/config/types"; -import {LovelaceViewConfig, LovelaceViewRawConfig} from "./types/homeassistant/data/lovelace/config/view"; -import {LovelaceCardConfig} from "./types/homeassistant/data/lovelace"; -import {StackCardConfig} from "./types/homeassistant/panels/lovelace/cards/types"; -import {generic} from "./types/strategy/generic"; -import {views} from "./types/strategy/views"; -import ViewConfig = views.ViewConfig; -import StrategyArea = generic.StrategyArea; -import SupportedDomains = generic.SupportedDomains; +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 { sanitizeClassName } from './utilities/auxiliaries'; +import { logMessage, lvlError } from './utilities/debug'; +import RegistryFilter from './utilities/RegistryFilter'; /** * Mushroom Dashboard Strategy.
@@ -21,7 +23,7 @@ import SupportedDomains = generic.SupportedDomains; * The strategy makes use Mushroom and Mini Graph cards to represent your entities.
*
* Features:
- * 🛠 Automatically create dashboard with three lines of yaml.
+ * 🛠️ Automatically create dashboard with three lines of yaml.
* 😍 Built-in Views for several standard domains.
* 🎨 Many options to customize to your needs.
*
@@ -31,242 +33,176 @@ class MushroomStrategy extends HTMLTemplateElement { /** * Generate a dashboard. * - * Called when opening a dashboard. + * This method creates views for each exposed domain and area. + * It also adds custom views if specified in the strategy options. * - * @param {generic.DashboardInfo} info Dashboard strategy information object. - * @return {Promise} + * @param {DashboardInfo} info Dashboard strategy information object. + * + * @remarks + * Called when opening a dashboard. */ - static async generateDashboard(info: generic.DashboardInfo): Promise { - await Helper.initialize(info); + static async generateDashboard(info: DashboardInfo): Promise { + await Registry.initialize(info); - // Create views. const views: LovelaceViewRawConfig[] = []; - let viewModule; + // Parallelize view imports and creation. + const viewPromises = Registry.getExposedNames('view') + .filter(isSupportedView) + .map(async (viewName) => { + try { + const moduleName = sanitizeClassName(`${viewName}View`); + const View = (await import(`./views/${moduleName}`)).default; + const currentView = new View(Registry.strategyOptions.views[viewName]); + const viewConfiguration = await currentView.getView(); - // Create a view for each exposed domain. - for (let viewId of Helper.getExposedViewIds()) { - try { - const viewType = Helper.sanitizeClassName(viewId + "View"); - viewModule = await import(`./views/${viewType}`); - const view: ViewConfig = await new viewModule[viewType](Helper.strategyOptions.views[viewId]).getView(); - - if (view.cards?.length) { - views.push(view); + if (viewConfiguration.cards.length) { + return viewConfiguration; + } + } catch (e) { + logMessage(lvlError, `Error importing ${viewName} view!`, e); } - } catch (e) { - Helper.logError(`View '${viewId}' couldn't be loaded!`, e); - } + + return null; + }); + + const resolvedViews = (await Promise.all(viewPromises)).filter(Boolean) as LovelaceViewRawConfig[]; + + views.push(...resolvedViews); + + // Subviews for areas + views.push( + ...Registry.areas.map((area) => ({ + title: area.name, + path: area.area_id, + subview: true, + strategy: { + type: 'custom:mushroom-strategy', + options: { area }, + }, + })), + ); + + // Extra views + if (Registry.strategyOptions.extra_views) { + views.push(...Registry.strategyOptions.extra_views); } - // Create subviews for each area. - for (let area of Helper.areas) { - if (!area.hidden) { - views.push({ - title: area.name, - path: area.area_id ?? area.name, - subview: true, - strategy: { - type: "custom:mushroom-strategy", - options: { - area, - }, - }, - }); - } - } - - // Add custom views. - if (Helper.strategyOptions.extra_views) { - views.push(...Helper.strategyOptions.extra_views); - } - - // Return the created views. - return { - views: views, - }; + return { views }; } /** * Generate a view. * - * Called when opening a subview. + * 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 {generic.ViewInfo} info The view's strategy information object. - * @return {Promise} + * @param {ViewInfo} info The view's strategy information object. + * + * @remarks + * Called upon opening a subview. */ - static async generateView(info: generic.ViewInfo): Promise { - const exposedDomainIds = Helper.getExposedDomainIds(); - const area = info.view.strategy?.options?.area ?? {} as StrategyArea; + 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 ?? [])]; - // Set the target for controller cards to the current area. - let target: HassServiceTarget = { - area_id: [area.area_id], - }; + // Set the target for any Header card to the current area. + const target: HassServiceTarget = { area_id: [area.area_id] }; - // Create cards for each domain. - for (const domain of exposedDomainIds) { - if (domain === "default") { - continue; + // 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).toList(); + + if (!entities.length) { + return null; } - const className = Helper.sanitizeClassName(domain + "Card"); - - let domainCards: EntityCardConfig[] = []; + const titleCard = new HeaderCard( + { entity_id: entities.map((entity) => entity.entity_id) }, + Registry.strategyOptions.domains[domain], + ).createCard(); try { - domainCards = await import(`./cards/${className}`).then(cardModule => { - let domainCards: EntityCardConfig[] = []; - let entities = Helper.getDeviceEntities(area, domain); + const DomainCard = (await import(`./cards/${moduleName}`)).default; - // Exclude hidden Config and Diagnostic entities. - entities = applyEntityCategoryFilters(entities, domain); - - // Set the target for controller cards to entities without an area. - if (area.area_id === "undisclosed") { - target = { - entity_id: entities.map(entity => entity.entity_id), - } - } - - if (entities.length) { - // Create a Controller card for the current domain. - const titleCard = new ControllerCard( - target, - Helper.strategyOptions.domains[domain], - ).createCard(); - - if (domain === "sensor") { - // Create a card for each sensor-entity of the current area. - const sensorStates = Helper.getStateEntities(area, "sensor"); - const sensorCards: EntityCardConfig[] = []; - - for (const sensor of entities) { - // Find the state of the current sensor. - const sensorState = sensorStates.find(state => state.entity_id === sensor.entity_id); - let cardOptions = Helper.strategyOptions.card_options?.[sensor.entity_id]; - - if (sensorState?.attributes.unit_of_measurement) { - cardOptions = { - ...{ - type: "custom:mini-graph-card", - entities: [sensor.entity_id], - }, - ...cardOptions, - }; - - sensorCards.push(new SensorCard(sensor, cardOptions).getCard()); - } - } - - if (sensorCards.length) { - domainCards.push({ - type: "vertical-stack", - cards: sensorCards, - }); - - domainCards.unshift(titleCard); - } - - return domainCards; - } - - // Create a card for each other domain-entity of the current area. - for (const entity of entities) { - let deviceOptions; - let cardOptions = Helper.strategyOptions.card_options?.[entity.entity_id]; - - if (entity.device_id) { - deviceOptions = Helper.strategyOptions.card_options?.[entity.device_id]; - } - - domainCards.push(new cardModule[className](entity, cardOptions).getCard()); - } - - if (domain === "binary_sensor") { - // Horizontally group every two binary sensor cards. - const horizontalCards: EntityCardConfig[] = []; - - for (let i = 0; i < domainCards.length; i += 2) { - horizontalCards.push({ - type: "horizontal-stack", - cards: domainCards.slice(i, i + 2), - }); - } - - domainCards = horizontalCards; - } - - if (domainCards.length) { - domainCards.unshift(titleCard); - } - } - - return domainCards; - }); - } catch (e) { - Helper.logError("An error occurred while creating the domain cards!", e); - } - - if (domainCards.length) { - viewCards.push({ - type: "vertical-stack", - cards: domainCards, - }); - } - } - - if (!Helper.strategyOptions.domains.default.hidden) { - // Create cards for any other domain. - // Collect entities of the current area and unexposed domains. - let miscellaneousEntities = Helper.getDeviceEntities(area).filter( - entity => !exposedDomainIds.includes(entity.entity_id.split(".", 1)[0] as SupportedDomains) - ); - - // Exclude hidden Config and Diagnostic entities. - miscellaneousEntities = applyEntityCategoryFilters(miscellaneousEntities, "default"); - - // Create a column of miscellaneous entity cards. - if (miscellaneousEntities.length) { - let miscellaneousCards: (StackCardConfig | EntityCardConfig)[] = []; - - try { - miscellaneousCards = await import("./cards/MiscellaneousCard").then(cardModule => { - const miscellaneousCards: (StackCardConfig | EntityCardConfig)[] = [ - new ControllerCard(target, Helper.strategyOptions.domains.default).createCard(), - ]; - - for (const entity of miscellaneousEntities) { - let cardOptions = Helper.strategyOptions.card_options?.[entity.entity_id]; - - miscellaneousCards.push(new cardModule.MiscellaneousCard(entity, cardOptions).getCard()); - } - - return miscellaneousCards; - }); - } catch (e) { - Helper.logError("An error occurred while creating the domain cards!", e); + 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; } - viewCards.push({ - type: "vertical-stack", - cards: miscellaneousCards, + 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 = Registry.stackHorizontal(domainCards, 2); + } + + 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. - return { - cards: viewCards, - }; + return { cards: viewCards }; } } -customElements.define("ll-strategy-mushroom-strategy", MushroomStrategy); +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;" + '%c Mushroom Strategy %c '.concat(version, ' '), + 'color: white; background: coral; font-weight: 700;', + 'color: coral; background: white; font-weight: 700;', );