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;',
);