mirror of
https://github.com/DigiLive/mushroom-strategy.git
synced 2025-08-04 12:04:28 +02:00
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:
@@ -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;',
|
||||
);
|
||||
|
Reference in New Issue
Block a user