mirror of
https://github.com/DigiLive/mushroom-strategy.git
synced 2025-08-04 20:14: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 { 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;',
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user