mirror of
https://github.com/DigiLive/mushroom-strategy.git
synced 2025-08-04 20:14:28 +02:00
Add Registry class.
The class is an optimized version of the Helper class. - Sanitization of the HASS registries after import according to the configuration options. - Improved error handling and logging. - Removed generic utility functions.
This commit is contained in:
305
src/Registry.ts
Normal file
305
src/Registry.ts
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
import deepmerge from "deepmerge";
|
||||||
|
import {HassEntities} from "home-assistant-js-websocket";
|
||||||
|
import {ConfigurationDefaults} from "./configurationDefaults";
|
||||||
|
import {AreaRegistryEntry} from "./types/homeassistant/data/area_registry";
|
||||||
|
import {DeviceRegistryEntry} from "./types/homeassistant/data/device_registry";
|
||||||
|
import {EntityRegistryEntry} from "./types/homeassistant/data/entity_registry";
|
||||||
|
import {LovelaceCardConfig} from "./types/homeassistant/data/lovelace/config/card";
|
||||||
|
import {StackCardConfig} from "./types/homeassistant/panels/lovelace/cards/types";
|
||||||
|
import {
|
||||||
|
AllDomainsConfig,
|
||||||
|
DashboardInfo,
|
||||||
|
isSortable,
|
||||||
|
SingleDomainConfig,
|
||||||
|
StrategyArea,
|
||||||
|
StrategyConfig,
|
||||||
|
StrategyViewConfig,
|
||||||
|
SupportedDomains,
|
||||||
|
SupportedViews,
|
||||||
|
} from "./types/strategy/strategy-generics";
|
||||||
|
import {logMessage, lvlDebug, lvlFatal, lvlOff, lvlWarn, setDebugLevel} from "./utilities/debug";
|
||||||
|
import setupCustomLocalize from "./utilities/localize";
|
||||||
|
import RegistryFilter from "./utilities/RegistryFilter";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry Class
|
||||||
|
*
|
||||||
|
* Contains the entries of Home Assistant's registries and Strategy configuration.
|
||||||
|
*/
|
||||||
|
class Registry {
|
||||||
|
/** Entries of Home Assistant's entity registry. */
|
||||||
|
private static _entities: EntityRegistryEntry[];
|
||||||
|
/** Entries of Home Assistant's device registry. */
|
||||||
|
private static _devices: DeviceRegistryEntry[];
|
||||||
|
/** Entries of Home Assistant's area registry. */
|
||||||
|
private static _areas: StrategyArea[] = [];
|
||||||
|
/** Entries of Home Assistant's state registry */
|
||||||
|
private static _hassStates: HassEntities;
|
||||||
|
/** Indicates whether this module is initialized. */
|
||||||
|
private static _initialized: boolean = false;
|
||||||
|
/** The Custom strategy configuration. */
|
||||||
|
private static _strategyOptions: StrategyConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class constructor.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* This class shouldn't be instantiated directly.
|
||||||
|
* Instead, method {@link Registry.initialize} must be invoked.
|
||||||
|
*/
|
||||||
|
// noinspection JSUnusedLocalSymbols
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
/** The configuration of the strategy. */
|
||||||
|
static get strategyOptions(): StrategyConfig {
|
||||||
|
return Registry._strategyOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Home Assistant's Area registry.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* This module makes changes to the registry at {@link Registry.initialize}.
|
||||||
|
*/
|
||||||
|
static get areas(): StrategyArea[] {
|
||||||
|
return Registry._areas;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Home Assistant's Device registry.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* This module makes changes to the registry at {@link Registry.initialize}.
|
||||||
|
*/
|
||||||
|
static get devices(): DeviceRegistryEntry[] {
|
||||||
|
return Registry._devices;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Home Assistant's Entity registry.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* This module makes changes to the registry at {@link Registry.initialize}.
|
||||||
|
*/
|
||||||
|
static get entities(): EntityRegistryEntry[] {
|
||||||
|
return Registry._entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Home Assistant's State registry. */
|
||||||
|
static get hassStates(): HassEntities {
|
||||||
|
return Registry._hassStates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the initialization status of the Registry class. */
|
||||||
|
static get initialized(): boolean {
|
||||||
|
return Registry._initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize this module.
|
||||||
|
*
|
||||||
|
* Imports the registries of Home Assistant and the strategy options.
|
||||||
|
*
|
||||||
|
* After importing, the registries are sanitized according to the provided strategy options.
|
||||||
|
* This method must be called before using any other Registry functionality that depends on the imported data.
|
||||||
|
*
|
||||||
|
* @param {DashboardInfo} info Strategy information object.
|
||||||
|
*/
|
||||||
|
static async initialize(info: DashboardInfo): Promise<void> {
|
||||||
|
setDebugLevel(lvlFatal);
|
||||||
|
setupCustomLocalize(info.hass);
|
||||||
|
|
||||||
|
// Import the Hass States and strategy options.
|
||||||
|
Registry._hassStates = info.hass.states;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Registry._strategyOptions = deepmerge(ConfigurationDefaults, info.config?.strategy?.options ?? {});
|
||||||
|
} catch (e) {
|
||||||
|
logMessage(lvlFatal, 'Error importing strategy options!', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDebugLevel(Registry.strategyOptions.debug ? lvlDebug : lvlOff);
|
||||||
|
|
||||||
|
// Import the registries of Home Assistant.
|
||||||
|
try {
|
||||||
|
// noinspection ES6MissingAwait False positive? https://youtrack.jetbrains.com/issue/WEB-63746
|
||||||
|
[Registry._entities, Registry._devices, Registry._areas] = await Promise.all([
|
||||||
|
info.hass.callWS({ type: 'config/entity_registry/list' }) as Promise<EntityRegistryEntry[]>,
|
||||||
|
info.hass.callWS({ type: 'config/device_registry/list' }) as Promise<DeviceRegistryEntry[]>,
|
||||||
|
info.hass.callWS({ type: 'config/area_registry/list' }) as Promise<AreaRegistryEntry[]>,
|
||||||
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
logMessage(lvlFatal, 'Error importing Home Assistant registries!', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process entries of the HASS entity registry.
|
||||||
|
Registry._entities = new RegistryFilter(Registry.entities)
|
||||||
|
.not()
|
||||||
|
.whereEntityCategory('config')
|
||||||
|
.not()
|
||||||
|
.whereEntityCategory('diagnostic')
|
||||||
|
.isNotHidden()
|
||||||
|
.whereDisabledBy(null)
|
||||||
|
.orderBy(['name', 'original_name'], 'asc')
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
Registry._entities = Registry.entities.map((entity) => ({
|
||||||
|
...entity,
|
||||||
|
area_id: entity.area_id ?? 'undisclosed',
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Process entries of the HASS device registry.
|
||||||
|
Registry._devices = new RegistryFilter(Registry.devices)
|
||||||
|
.isNotHidden()
|
||||||
|
.whereDisabledBy(null)
|
||||||
|
.orderBy(['name_by_user', 'name'], 'asc')
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
Registry._devices = Registry.devices.map((device) => ({
|
||||||
|
...device,
|
||||||
|
area_id: device.area_id ?? 'undisclosed',
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Process entries of the HASS area registry.
|
||||||
|
if (!Registry.strategyOptions.areas._?.hidden) {
|
||||||
|
// Create and add the undisclosed area if not hidden in the strategy options.
|
||||||
|
if (!Registry.strategyOptions.areas.undisclosed?.hidden) {
|
||||||
|
Registry.areas.push(ConfigurationDefaults.areas.undisclosed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge area configurations of the Strategy options into the entries of the area registry.
|
||||||
|
// TODO: Check for to do the same for devices.
|
||||||
|
Registry._areas = Registry.areas.map((area) => {
|
||||||
|
return { ...area, ...Registry.strategyOptions.areas['_'], ...Registry.strategyOptions.areas?.[area.area_id] };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure the custom configuration of the undisclosed area doesn't overwrite the area_id.
|
||||||
|
Registry.strategyOptions.areas.undisclosed.area_id = 'undisclosed';
|
||||||
|
|
||||||
|
// Remove hidden areas if configured as so and sort them by name.
|
||||||
|
|
||||||
|
Registry._areas = new RegistryFilter(Registry.areas).isNotHidden().orderBy(['order', 'name'], 'asc').toList();
|
||||||
|
} else {
|
||||||
|
Registry._areas = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort views by order first and then by title.
|
||||||
|
const sortViews = () => {
|
||||||
|
const viewEntries = Object.entries(Registry.strategyOptions.views);
|
||||||
|
|
||||||
|
Registry.strategyOptions.views = Object.fromEntries(
|
||||||
|
viewEntries.sort(([_, a], [__, b]) => {
|
||||||
|
return (a.order ?? Infinity) - (b.order ?? Infinity) || (a.title ?? '').localeCompare(b.title ?? '');
|
||||||
|
}),
|
||||||
|
) as Record<SupportedViews, StrategyViewConfig>;
|
||||||
|
};
|
||||||
|
|
||||||
|
sortViews();
|
||||||
|
|
||||||
|
// Sort domains by order first and then by title.
|
||||||
|
const sortDomains = () => {
|
||||||
|
const domainEntries = Object.entries(Registry.strategyOptions.domains);
|
||||||
|
Registry.strategyOptions.domains = Object.fromEntries(
|
||||||
|
domainEntries.sort(([, a], [, b]) => {
|
||||||
|
if (isSortable(a) && isSortable(b)) {
|
||||||
|
return (a.order ?? Infinity) - (b.order ?? Infinity) || (a.title ?? '').localeCompare(b.title ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0; // Maintain the original order when none or only one item is sortable.
|
||||||
|
}),
|
||||||
|
) as { [K in SupportedDomains]: K extends '_' ? AllDomainsConfig : SingleDomainConfig };
|
||||||
|
};
|
||||||
|
|
||||||
|
sortDomains();
|
||||||
|
|
||||||
|
Registry._initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a template string to define the number of a given domain's entities with a certain state.
|
||||||
|
*
|
||||||
|
* States are compared against a given value by a given operator.
|
||||||
|
* States `unavailable` and `unknown` are always excluded.
|
||||||
|
*
|
||||||
|
* @param {string} domain The domain of the entities.
|
||||||
|
* @param {string} operator The comparison operator between state and value.
|
||||||
|
* @param {string} value The value to which the state is compared against.
|
||||||
|
*/
|
||||||
|
static getCountTemplate(domain: SupportedDomains, operator: string, value: string): string {
|
||||||
|
// noinspection JSMismatchedCollectionQueryUpdate
|
||||||
|
/**
|
||||||
|
* Array of entity state-entries, filtered by domain.
|
||||||
|
*
|
||||||
|
* Each element contains a template-string which is used to access home assistant's state machine (state object) in
|
||||||
|
* a template; E.g. `states['light.kitchen']`.
|
||||||
|
*/
|
||||||
|
const states: string[] = [];
|
||||||
|
|
||||||
|
if (!Registry.initialized) {
|
||||||
|
logMessage(lvlWarn, 'Registry not initialized!');
|
||||||
|
|
||||||
|
return '?';
|
||||||
|
}
|
||||||
|
|
||||||
|
states.push(
|
||||||
|
...new RegistryFilter(Registry.entities)
|
||||||
|
.whereDomain(domain)
|
||||||
|
.toList()
|
||||||
|
.map((entity) => `states['${entity.entity_id}']`),
|
||||||
|
);
|
||||||
|
|
||||||
|
return `{% set entities = [${states}] %}
|
||||||
|
{{ entities
|
||||||
|
| selectattr('state','${operator}','${value}')
|
||||||
|
| selectattr('state','ne','unavailable')
|
||||||
|
| selectattr('state','ne','unknown')
|
||||||
|
| list
|
||||||
|
| count
|
||||||
|
}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits an array of card configurations into horizontal stack card configurations.
|
||||||
|
*
|
||||||
|
* Each horizontal stack contains a specified number of cards.
|
||||||
|
*
|
||||||
|
* @param {LovelaceCardConfig[]} cardConfigurations - Array of card configurations to be stacked.
|
||||||
|
* @param {number} columnCount - Number of cards per horizontal stack.
|
||||||
|
*/
|
||||||
|
static stackHorizontal(cardConfigurations: LovelaceCardConfig[], columnCount: number): StackCardConfig[] {
|
||||||
|
const stackedCardConfigurations: StackCardConfig[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < cardConfigurations.length; i += columnCount) {
|
||||||
|
stackedCardConfigurations.push({
|
||||||
|
type: 'horizontal-stack',
|
||||||
|
cards: cardConfigurations.slice(i, i + columnCount),
|
||||||
|
} as StackCardConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stackedCardConfigurations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the names of the specified type which aren't set to hidden in the strategy options.
|
||||||
|
*
|
||||||
|
* @param {string} type The type of options to filter ("domain", "view", "chip").
|
||||||
|
*
|
||||||
|
* @returns {string[]} For domains and views: names of items that aren't hidden.
|
||||||
|
* For chips: names of items that are explicitly set to true.
|
||||||
|
*/
|
||||||
|
static getExposedNames(type: 'domain' | 'view' | 'chip'): string[] {
|
||||||
|
// TODO: Align chip with other types.
|
||||||
|
if (type === 'chip') {
|
||||||
|
return Object.entries(Registry.strategyOptions.chips)
|
||||||
|
.filter(([_, value]) => value === true)
|
||||||
|
.map(([key]) => key.split('_')[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = Registry.strategyOptions[`${type}s`] as Record<string, { hidden?: boolean }>;
|
||||||
|
|
||||||
|
return Object.keys(group).filter((key) => key !== '_' && key !== 'default' && !group[key].hidden);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Registry };
|
Reference in New Issue
Block a user