diff --git a/src/Helper.ts b/src/Helper.ts deleted file mode 100644 index 1b109e8..0000000 --- a/src/Helper.ts +++ /dev/null @@ -1,522 +0,0 @@ -import {getConfigurationDefaults} from "./configurationDefaults"; -import {HassEntities, HassEntity} from "home-assistant-js-websocket"; -import deepmerge from "deepmerge"; -import {EntityRegistryEntry} from "./types/homeassistant/data/entity_registry"; -import {DeviceRegistryEntry} from "./types/homeassistant/data/device_registry"; -import {AreaRegistryEntry} from "./types/homeassistant/data/area_registry"; -import {generic} from "./types/strategy/generic"; -import setupCustomLocalize from "./localize"; -import {applyEntityCategoryFilters} from "./utillties/filters"; -import StrategyArea = generic.StrategyArea; -import ViewConfig = generic.StrategyViewConfig; -import SupportedDomains = generic.SupportedDomains; -import supportedViews = generic.SupportedViews; -import isSortable = generic.isSortable; -import AllDomainsConfig = generic.AllDomainsConfig; -import SingleDomainConfig = generic.SingleDomainConfig; -import isSupportedView = generic.isSupportedView; -import isSupportedDomain = generic.isSupportedDomain; - -/** - * Helper Class - * - * Contains the objects of Home Assistant's registries and helper methods. - */ -class Helper { - /** - * An array of entities from Home Assistant's entity registry. - * - * @type {EntityRegistryEntry[]} - * @private - */ - static #entities: EntityRegistryEntry[]; - - /** - * An array of entities from Home Assistant's device registry. - * - * @type {DeviceRegistryEntry[]} - * @private - */ - static #devices: DeviceRegistryEntry[]; - - /** - * An array of entities from Home Assistant's area registry. - * - * @type {StrategyArea[]} - * @private - */ - static #areas: StrategyArea[] = []; - - /** - * An array of state entities from Home Assistant's Hass-object. - * - * @type {HassEntities} - * @private - */ - static #hassStates: HassEntities; - - /** - * Indicates whether this module is initialized. - * - * @type {boolean} True if initialized. - * @private - */ - static #initialized: boolean = false; - - /** - * The Custom strategy configuration. - * - * @type {generic.StrategyConfig} - * @private - */ - static #strategyOptions: generic.StrategyConfig; - - /** - * Set to true for more verbose information in the console. - * - * @type {boolean} - * @private - */ - static #debug: boolean; - static customLocalize: Function; - - /** - * Class constructor. - * - * This class shouldn't be instantiated directly. - * Instead, it should be initialized with method initialize(). - * - * @throws {Error} If trying to instantiate this class. - */ - constructor() { - throw new Error("This class should be invoked with method initialize() instead of using the keyword new!"); - } - - /** - * Custom strategy configuration. - * - * @returns {generic.StrategyConfig} - * @static - */ - static get strategyOptions(): generic.StrategyConfig { - return this.#strategyOptions; - } - - /** - * Get the entities from Home Assistant's area registry. - * - * @returns {StrategyArea[]} - * @static - */ - static get areas(): StrategyArea[] { - return this.#areas; - } - - /** - * Get the devices from Home Assistant's device registry. - * - * @returns {DeviceRegistryEntry[]} - * @static - */ - static get devices(): DeviceRegistryEntry[] { - return this.#devices; - } - - /** - * Get the entities from Home Assistant's entity registry. - * - * @returns {EntityRegistryEntry[]} - * @static - */ - static get entities(): EntityRegistryEntry[] { - return this.#entities; - } - - /** - * Get the current debug mode of the mushroom strategy. - * - * @returns {boolean} - * @static - */ - static get debug(): boolean { - return this.#debug; - } - - /** - * Initialize this module. - * - * @param {generic.DashboardInfo} info Strategy information object. - * @returns {Promise} - * @static - */ - static async initialize(info: generic.DashboardInfo): Promise { - // Initialize properties. - this.customLocalize = setupCustomLocalize(info.hass); - - const configurationDefaults = getConfigurationDefaults(this.customLocalize) - this.#strategyOptions = deepmerge(configurationDefaults, info.config?.strategy?.options ?? {}); - - this.#hassStates = info.hass.states; - this.#debug = this.#strategyOptions.debug; - - try { - // Query the registries of Home Assistant. - - // noinspection ES6MissingAwait False positive? https://youtrack.jetbrains.com/issue/WEB-63746 - [Helper.#entities, Helper.#devices, Helper.#areas] = await Promise.all([ - info.hass.callWS({type: "config/entity_registry/list"}) as Promise, - info.hass.callWS({type: "config/device_registry/list"}) as Promise, - info.hass.callWS({type: "config/area_registry/list"}) as Promise, - ]); - } catch (e) { - Helper.logError("An error occurred while querying Home assistant's registries!", e); - throw 'Check the console for details'; - } - - // Create and add the undisclosed area if not hidden in the strategy options. - if (!this.#strategyOptions.areas.undisclosed?.hidden) { - this.#strategyOptions.areas.undisclosed = { - ...configurationDefaults.areas.undisclosed, - ...this.#strategyOptions.areas.undisclosed, - }; - - // Make sure the custom configuration of the undisclosed area doesn't overwrite the area_id. - this.#strategyOptions.areas.undisclosed.area_id = "undisclosed"; - - this.#areas.push(this.#strategyOptions.areas.undisclosed); - } - - // Merge custom areas of the strategy options into strategy areas. - this.#areas = Helper.areas.map(area => { - return {...area, ...this.#strategyOptions.areas?.[area.area_id]}; - }); - - // Sort strategy areas by order first and then by name. - this.#areas.sort((a, b) => { - return (a.order ?? Infinity) - (b.order ?? Infinity) || a.name.localeCompare(b.name); - }); - - // Sort custom and default views of the strategy options by order first and then by title. - this.#strategyOptions.views = Object.fromEntries( - Object.entries(this.#strategyOptions.views).sort(([, a], [, b]) => { - const viewA = a as ViewConfig; - const viewB = b as ViewConfig; - - return (viewA.order ?? Infinity) - (viewB.order ?? Infinity) - || (viewA.title ?? "undefined").localeCompare(viewB.title ?? "undefined"); - }), - ) as Record; - - // Sort custom and default domains of the strategy options by order first and then by title. - this.#strategyOptions.domains = Object.fromEntries( - Object.entries(this.#strategyOptions.domains).sort(([, a], [, b]) => { - if (isSortable(a) && isSortable(b)) { - const orderA = ('order' in a) ? a.order ?? Infinity : Infinity; - const orderB = ('order' in b) ? b.order ?? Infinity : Infinity; - - return orderA - orderB || (a.title ?? "undefined").localeCompare(b.title ?? "undefined"); - } - - return 0; - }), - ) as { [K in SupportedDomains]: K extends "_" ? AllDomainsConfig : SingleDomainConfig; }; - - this.#initialized = true; - } - - /** - * Get the initialization status of the Helper class. - * - * @returns {boolean} True if this module is initialized. - * @static - */ - static isInitialized(): boolean { - return this.#initialized; - } - - /** - * 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. - * - * @return {string} The template string. - * @static - */ - 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']" - * - * The array excludes hidden and disabled entities. - * - * @type {string[]} - */ - const states: string[] = []; - - if (!this.isInitialized()) { - console.warn("Helper class should be initialized before calling this method!"); - } - - // Get the state of entities which are linked to the given area. - for (const area of this.#areas) { - let entities = this.getDeviceEntities(area, domain); - - // Exclude hidden Config and Diagnostic entities. - entities = applyEntityCategoryFilters(entities, domain); - - const newStates = entities.map((entity) => `states['${entity.entity_id}']`); - - states.push(...newStates); - } - - return ( - `{% set entities = [${states}] %} - {{ entities - | selectattr('state','${operator}','${value}') - | selectattr('state','ne','unavailable') - | selectattr('state','ne','unknown') - | list - | count - }}` - ); - } - - /** - * Get device entities from the entity registry, filtered by area and domain. - * - * The entity registry is a registry where Home-Assistant keeps track of all entities. - * A device is represented in Home Assistant via one or more entities. - * - * The result excludes hidden and disabled entities. - * - * @param {AreaRegistryEntry} area Area entity. - * @param {string} [domain] The domain of the entity-id. - * - * @return {EntityRegistryEntry[]} Array of device entities. - * @static - */ - static getDeviceEntities(area: AreaRegistryEntry, domain?: string): EntityRegistryEntry[] { - if (!this.isInitialized()) { - console.warn("Helper class should be initialized before calling this method!"); - } - - // Get the ID of the devices which are linked to the given area. - const areaDeviceIds = this.#devices.filter((device) => { - return (device.area_id ?? "undisclosed") === area.area_id; - }).map((device: DeviceRegistryEntry) => { - - return device.id; - }); - - // Return the entities of which all conditions of the callback function are met. @see areaFilterCallback. - return this.#entities.filter( - this.#areaFilterCallback, { - area: area, - domain: domain, - areaDeviceIds: areaDeviceIds, - }) - .sort((a, b) => { - return (a.original_name ?? "undefined").localeCompare(b.original_name ?? "undefined"); - }); - } - - /** - * Get state entities, filtered by area and domain. - * - * The result excludes hidden and disabled entities. - * - * @param {AreaRegistryEntry} area Area entity. - * @param {string} domain Domain of the entity-id. - * - * @return {HassEntity[]} Array of state entities. - */ - static getStateEntities(area: AreaRegistryEntry, domain: string): HassEntity[] { - if (!this.isInitialized()) { - console.warn("Helper class should be initialized before calling this method!"); - } - - const states: HassEntity[] = []; - - // Create a map for the hassEntities and devices {id: object} to improve lookup speed. - const entityMap: { - [s: string]: EntityRegistryEntry; - } = Object.fromEntries(this.#entities.map((entity) => [entity.entity_id, entity])); - const deviceMap: { - [s: string]: DeviceRegistryEntry; - } = Object.fromEntries(this.#devices.map((device) => [device.id, device])); - - // Get states whose entity-id starts with the given string. - const stateEntities = Object.values(this.#hassStates).filter( - (state) => state.entity_id.startsWith(`${domain}.`), - ); - - for (const state of stateEntities) { - const hassEntity = entityMap[state.entity_id]; - const device = deviceMap[hassEntity?.device_id ?? ""]; - - // Collect states of which any (whichever comes first) of the conditions below are met: - // 1. The linked entity is linked to the given area. - // 2. The entity is linked to a device, and the linked device is linked to the given area. - if ( - (hassEntity?.area_id === area.area_id) - || (device && device.area_id === area.area_id) - ) { - states.push(state); - } - } - - return states; - } - - /** - * Get the state object of a HASS entity. - * - * @param {EntityRegistryEntry} entity The entity for which to get the state. - * @returns {HassEntity | undefined} The state object of the entity, or undefined if not found. - * @static - */ - static getEntityState(entity: EntityRegistryEntry): HassEntity | undefined { - return this.#hassStates[entity.entity_id]; - } - - /** - * Sanitize a classname. - * - * The name is sanitized by capitalizing the first character of the name or after an underscore. - * Underscores are removed. - * - * @param {string} className Name of the class to sanitize. - * @returns {string} The sanitized classname. - */ - static sanitizeClassName(className: string): string { - className = className.charAt(0).toUpperCase() + className.slice(1); - - return className.replace(/([-_][a-z])/g, (group) => group - .toUpperCase() - .replace("-", "") - .replace("_", ""), - ); - } - - /** - * Get the ids of the views which aren't set to hidden in the strategy options. - * - * @return {SupportedViews[]} An array of view ids. - */ - static getExposedViewIds(): supportedViews[] { - if (!this.isInitialized()) { - console.warn("Helper class should be initialized before calling this method!"); - } - - const ids = this.#getObjectKeysByPropertyValue(this.#strategyOptions.views, "hidden", false); - - return ids.filter(isSupportedView); - } - - /** - * Get the ids of the domain ids which aren't set to hidden in the strategy options. - * - * @return {SupportedDomains[]} An array of domain ids. - */ - static getExposedDomainIds(): SupportedDomains[] { - if (!this.isInitialized()) { - console.warn("Helper class should be initialized before calling this method!"); - } - - const ids = this.#getObjectKeysByPropertyValue(this.#strategyOptions.domains, "hidden", false); - - return ids.filter(isSupportedDomain); - } - - /** - * Callback function for filtering entities. - * - * Entities of which all the conditions below are met are kept: - * 1. The entity is not hidden and the entity's device is not hidden by the strategy options. - * 2. The entity is not hidden and is not disabled by Hass. - * 3. The entity's domain matches the given domain. - * 4. The entity itself or else the entity's device is linked to the given area. - * - * @param {EntityRegistryEntry} entity The current Hass entity to evaluate. - * @this {AreaFilterContext} - * - * @return {boolean} True to keep the entity. - * @static - */ - static #areaFilterCallback( - this: { - area: AreaRegistryEntry, - areaDeviceIds: string[], - domain: string, - }, - entity: EntityRegistryEntry): boolean { - const cardOptions = Helper.strategyOptions.card_options?.[entity.entity_id]; - const deviceOptions = Helper.strategyOptions.card_options?.[entity.device_id ?? "null"]; - - const entityUnhidden = - !cardOptions?.hidden && !deviceOptions?.hidden // Condition 1. - && entity.hidden_by === null && entity.disabled_by === null; // Condition 2. - const domainMatches = this.domain === undefined || entity.entity_id.startsWith(`${this.domain}.`); // Condition 3. - // Condition 4. - const entityLinked = this.area.area_id === "undisclosed" - // Undisclosed area. - ? !entity.area_id && (this.areaDeviceIds.includes(entity.device_id ?? "") || !entity.device_id) - // Area is a hass entity. Note: entity.area_id is set to null when using device's area. - : entity.area_id === this.area.area_id || (!entity.area_id && this.areaDeviceIds.includes(entity.device_id ?? "")); - - return (entityUnhidden && domainMatches && entityLinked); - } - - /** - * Get the keys of nested objects by its property value. - * - * @param {Object} object An object of objects. - * @param {string|number} property The name of the property to evaluate. - * @param {*} value The value which the property should match. - * - * @return {string[]} An array with keys. - */ - static #getObjectKeysByPropertyValue( - object: { [k: string]: any }, - property: string, value: any - ): string[] { - const keys: string[] = []; - - for (const key of Object.keys(object)) { - if (object[key][property] === value) { - keys.push(key); - } - } - - return keys; - } - - /** - * Logs an error message to the console. - * - * @param {string} userMessage - The error message to display. - * @param {unknown} [e] - (Optional) The error object or additional information. - * - * @return {void} - */ - static logError(userMessage: string, e?: unknown): void { - if (Helper.debug) { - console.error(userMessage, e); - - return; - } - - console.error(userMessage); - } -} - -export {Helper}; diff --git a/src/localize.ts b/src/localize.ts deleted file mode 100644 index ccf7471..0000000 --- a/src/localize.ts +++ /dev/null @@ -1,60 +0,0 @@ -import * as de from "./translations/de.json"; -import * as en from "./translations/en.json"; -import * as es from "./translations/es.json"; -import * as nl from "./translations/nl.json"; -import {HomeAssistant} from "./types/homeassistant/types"; - -/* Registry of currently supported languages */ -const languages: Record = { - en, - es, - nl, - de, -}; - -/* The fallback language if the user-defined language isn't defined */ -const DEFAULT_LANG = "en"; - -/** - * Get a string by keyword and language. - * - * @param {string} key The keyword to look for in object notation (E.g. generic.home). - * @param {string} lang The language to get the string from (E.g. en). - * - * @returns {string | undefined} The requested string or undefined if the keyword doesn't exist/on error. - */ -function getTranslatedString(key: string, lang: string): string | undefined { - try { - return key - .split(".") - .reduce( - (o, i) => (o as Record)[i], - languages[lang] - ) as string; - } catch (_) { - - return undefined; - } -} - -/** - * Set up the localization. - * - * It reads the user-defined language with a fall-back to english and returns a function to get strings from - * language-files by keyword. - * - * If the keyword is undefined, or on error, the keyword itself is returned. - * - * @param {HomeAssistant} hass The Home Assistant object. - * @returns {(key: string) => string} The function to call for translating strings. - */ -export default function setupCustomLocalize(hass?: HomeAssistant): (key: string) => string { - return function (key: string) { - const lang = hass?.locale.language ?? DEFAULT_LANG; - - let translated = getTranslatedString(key, lang); - if (!translated) translated = getTranslatedString(key, DEFAULT_LANG); - - return translated ?? key; - }; -} diff --git a/src/utillties/filters.ts b/src/utillties/filters.ts deleted file mode 100644 index 8786d58..0000000 --- a/src/utillties/filters.ts +++ /dev/null @@ -1,64 +0,0 @@ -import {EntityRegistryEntry} from "../types/homeassistant/data/entity_registry"; -import {Helper} from "../Helper"; -import {generic} from "../types/strategy/generic"; -import SupportedDomains = generic.SupportedDomains; - -/** - * Filter an array of entities by property/value pair - * - * @param entities The array of entities to filter. - * @param property The property to filter on. - * @param value The value to match. - * @param exclude Whether to exclude entities with the given property/value pair (default: true). - * - * @returns A new list of entities filtered by the given property/value pair. - */ -export function filterEntitiesByPropertyValue( - entities: EntityRegistryEntry[], - property: keyof EntityRegistryEntry, - value: any, - exclude: boolean = true -) { - return entities.filter(entity => exclude ? entity[property] !== value : entity[property] === value); -} - -export function applyEntityCategoryFilters(entities: EntityRegistryEntry[], domain: SupportedDomains) { - if (!Helper.isInitialized()) { - throw new Error("The Helper module must be initialized before using this one."); - } - - const domainOptions = { - ...Helper.strategyOptions.domains["_"], - ...Helper.strategyOptions.domains[domain], - }; - - let filteredEntityCategory = []; - - if (domainOptions.hide_config_entities) { - entities = filterEntitiesByPropertyValue(entities, "entity_category", "config"); - filteredEntityCategory.push("Config"); - } - - if (domainOptions.hide_diagnostic_entities) { - entities = filterEntitiesByPropertyValue(entities, "entity_category", "diagnostic"); - filteredEntityCategory.push("Diagnostic"); - } - - if (Helper.debug && filteredEntityCategory.length > 0) { - console.warn(filteredEntityCategory.join(" & ") + " entities are filtered out."); - } - - return entities; -} - -/*export function filterHiddenEntities(entities: EntityRegistryEntry[]) { - entities = entities.filter( - function (entity) { - return entity.hidden_by === null // entity is not hidden by HASS settings. - && entity.disabled_by === null // entity is not disabled by HASS settings. - && Helper.strategyOptions.card_options.[entity.entity_id] // entity is not hidden by strategy options. - } - ); -}*/ - -