Optimize MushroomStrategy class

Changed code to make use of new optimized modules.
- Parallelization of dynamic imports for views and cards.
- Improved error handling and logging throughout the codebase.
- Enforced stricter and simplified types and interfaces.
- Localization now available at global scope.
- Typos and Grammar.
This commit is contained in:
DigiLive
2025-04-23 08:05:32 +02:00
parent 22f523d0ee
commit 98d005b01a

View File

@@ -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.<br>
@@ -21,7 +23,7 @@ import SupportedDomains = generic.SupportedDomains;
* The strategy makes use Mushroom and Mini Graph cards to represent your entities.<br>
* <br>
* Features:<br>
* 🛠 Automatically create dashboard with three lines of yaml.<br>
* 🛠 Automatically create dashboard with three lines of yaml.<br>
* 😍 Built-in Views for several standard domains.<br>
* 🎨 Many options to customize to your needs.<br>
* <br>
@@ -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<LovelaceConfig>}
* @param {DashboardInfo} info Dashboard strategy information object.
*
* @remarks
* Called when opening a dashboard.
*/
static async generateDashboard(info: generic.DashboardInfo): Promise<LovelaceConfig> {
await Helper.initialize(info);
static async generateDashboard(info: DashboardInfo): Promise<LovelaceConfig> {
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<LovelaceViewConfig>}
* @param {ViewInfo} info The view's strategy information object.
*
* @remarks
* Called upon opening a subview.
*/
static async generateView(info: generic.ViewInfo): Promise<LovelaceViewConfig> {
const exposedDomainIds = Helper.getExposedDomainIds();
const area = info.view.strategy?.options?.area ?? {} as StrategyArea;
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 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;',
);