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 { HassServiceTarget } from 'home-assistant-js-websocket';
import {SensorCard} from "./cards/SensorCard"; import HeaderCard from './cards/HeaderCard';
import {ControllerCard} from "./cards/ControllerCard"; import SensorCard from './cards/SensorCard';
import {EntityCardConfig} from "./types/lovelace-mushroom/cards/entity-card-config"; import { Registry } from './Registry';
import {HassServiceTarget} from "home-assistant-js-websocket"; import { LovelaceCardConfig } from './types/homeassistant/data/lovelace/config/card';
import {applyEntityCategoryFilters} from "./utillties/filters"; 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 {LovelaceViewConfig, LovelaceViewRawConfig} from "./types/homeassistant/data/lovelace/config/view"; import {
import {LovelaceCardConfig} from "./types/homeassistant/data/lovelace"; DashboardInfo,
import {StackCardConfig} from "./types/homeassistant/panels/lovelace/cards/types"; isSupportedDomain,
import {generic} from "./types/strategy/generic"; isSupportedView,
import {views} from "./types/strategy/views"; StrategyArea,
import ViewConfig = views.ViewConfig; ViewInfo,
import StrategyArea = generic.StrategyArea; } from './types/strategy/strategy-generics';
import SupportedDomains = generic.SupportedDomains; import { sanitizeClassName } from './utilities/auxiliaries';
import { logMessage, lvlError } from './utilities/debug';
import RegistryFilter from './utilities/RegistryFilter';
/** /**
* Mushroom Dashboard Strategy.<br> * 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> * The strategy makes use Mushroom and Mini Graph cards to represent your entities.<br>
* <br> * <br>
* Features:<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> * 😍 Built-in Views for several standard domains.<br>
* 🎨 Many options to customize to your needs.<br> * 🎨 Many options to customize to your needs.<br>
* <br> * <br>
@@ -31,242 +33,176 @@ class MushroomStrategy extends HTMLTemplateElement {
/** /**
* Generate a dashboard. * 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. * @param {DashboardInfo} info Dashboard strategy information object.
* @return {Promise<LovelaceConfig>} *
* @remarks
* Called when opening a dashboard.
*/ */
static async generateDashboard(info: generic.DashboardInfo): Promise<LovelaceConfig> { static async generateDashboard(info: DashboardInfo): Promise<LovelaceConfig> {
await Helper.initialize(info); await Registry.initialize(info);
// Create views.
const views: LovelaceViewRawConfig[] = []; 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. if (viewConfiguration.cards.length) {
for (let viewId of Helper.getExposedViewIds()) { return viewConfiguration;
try { }
const viewType = Helper.sanitizeClassName(viewId + "View"); } catch (e) {
viewModule = await import(`./views/${viewType}`); logMessage(lvlError, `Error importing ${viewName} view!`, e);
const view: ViewConfig = await new viewModule[viewType](Helper.strategyOptions.views[viewId]).getView();
if (view.cards?.length) {
views.push(view);
} }
} 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. return { views };
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,
};
} }
/** /**
* Generate a view. * 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. * @param {ViewInfo} info The view's strategy information object.
* @return {Promise<LovelaceViewConfig>} *
* @remarks
* Called upon opening a subview.
*/ */
static async generateView(info: generic.ViewInfo): Promise<LovelaceViewConfig> { static async generateView(info: ViewInfo): Promise<LovelaceViewConfig> {
const exposedDomainIds = Helper.getExposedDomainIds(); const exposedDomainNames = Registry.getExposedNames('domain');
const area = info.view.strategy?.options?.area ?? {} as StrategyArea; 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 ?? [])]; const viewCards: LovelaceCardConfig[] = [...(area.extra_cards ?? [])];
// Set the target for controller cards to the current area. // Set the target for any Header card to the current area.
let target: HassServiceTarget = { const target: HassServiceTarget = { area_id: [area.area_id] };
area_id: [area.area_id],
};
// Create cards for each domain. // Prepare promises for all supported domains
for (const domain of exposedDomainIds) { const domainCardPromises = exposedDomainNames.filter(isSupportedDomain).map(async (domain) => {
if (domain === "default") { const moduleName = sanitizeClassName(domain + 'Card');
continue; const entities = new RegistryFilter(areaEntities).whereDomain(domain).toList();
if (!entities.length) {
return null;
} }
const className = Helper.sanitizeClassName(domain + "Card"); const titleCard = new HeaderCard(
{ entity_id: entities.map((entity) => entity.entity_id) },
let domainCards: EntityCardConfig[] = []; Registry.strategyOptions.domains[domain],
).createCard();
try { try {
domainCards = await import(`./cards/${className}`).then(cardModule => { const DomainCard = (await import(`./cards/${moduleName}`)).default;
let domainCards: EntityCardConfig[] = [];
let entities = Helper.getDeviceEntities(area, domain);
// Exclude hidden Config and Diagnostic entities. if (domain === 'sensor') {
entities = applyEntityCategoryFilters(entities, domain); const domainCards = entities
.filter((entity) => Registry.hassStates[entity.entity_id]?.attributes.unit_of_measurement)
// Set the target for controller cards to entities without an area. .map((entity) => {
if (area.area_id === "undisclosed") { const options = {
target = { ...(entity.device_id && Registry.strategyOptions.card_options?.[entity.device_id]),
entity_id: entities.map(entity => entity.entity_id), ...Registry.strategyOptions.card_options?.[entity.entity_id],
} type: 'custom:mini-graph-card',
} entities: [entity.entity_id],
};
if (entities.length) { return new SensorCard(entity, options).getCard();
// Create a Controller card for the current domain. });
const titleCard = new ControllerCard( return domainCards.length ? { type: 'vertical-stack', cards: [titleCard, ...domainCards] } : null;
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);
} }
viewCards.push({ let domainCards = entities.map((entity) => {
type: "vertical-stack", const cardOptions = {
cards: miscellaneousCards, ...(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'; 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: coral; background: white; font-weight: 700;" 'color: white; background: coral; font-weight: 700;',
'color: coral; background: white; font-weight: 700;',
); );