Add modules

- debug module for centralized logging and debug level management.
- localize module for localization functions in the global scope.
- RegistryFilter module for advanced filtering and sorting of HASS
  registries.
- auxiliaries module for genric utility functions.
This commit is contained in:
DigiLive
2025-04-23 07:10:53 +02:00
parent b068486aeb
commit 500a221638
3 changed files with 15 additions and 87 deletions

View File

@@ -1,10 +1,8 @@
// noinspection JSUnusedGlobalSymbols
import { Registry } from '../Registry';
import { DeviceRegistryEntry } from '../types/homeassistant/data/device_registry';
import { EntityCategory, EntityRegistryEntry } from '../types/homeassistant/data/entity_registry';
import { RegistryEntry, StrategyConfig } from '../types/strategy/strategy-generics';
import { logMessage, lvlWarn } from './debug';
import { logMessage, lvlDebug } from './debug';
/**
* A class for filtering and sorting arrays of Home Assistant's registry entries.
@@ -27,7 +25,7 @@ class RegistryFilter<T extends RegistryEntry, K extends keyof T = keyof T> {
constructor(entries: T[]) {
this.entries = entries;
this.entryIdentifier = (
entries.length === 0 || 'entity_id' in entries[0] ? 'entity_id' : 'floor_id' in entries[0] ? 'floor_id' : 'id'
entries.length == 0 || 'entity_id' in entries[0] ? 'entity_id' : 'floor_id' in entries[0] ? 'floor_id' : 'id'
) as ('entity_id' | 'floor_id' | 'id') & K;
}
@@ -70,36 +68,25 @@ class RegistryFilter<T extends RegistryEntry, K extends keyof T = keyof T> {
* Filters entries **strictly** by their `area_id`.
*
* - Entries with a matching `area_id` are kept.
* - If `expandToDevice` is `true`, the device's `area_id` is evaluated if the entry's area_id doesn't match.
* - If `areaId` is `undefined` (or omitted), entries without an `area_id` property are kept.
* - If `expandToDevice` is `true` and the entry's `area_id` is `undisclosed`, the device's `area_id` is used.
*
* @param {string | undefined} areaId - The area id to match.
* @param {boolean} [expandToDevice=true] - Whether to use the device's `area_id` if the entry's doesn't match.
*
* @remarks
* For area id `undisclosed`, the `area_id` of the entry's device may be `undisclosed` or `undefined`.
* @param {boolean} [expandToDevice=true] - Whether to use the device's `area_id` if the entry has none.
*/
whereAreaId(areaId?: string, expandToDevice: boolean = true): this {
const predicate = (entry: T) => {
let deviceAreaId: string | null | undefined = undefined;
const entryObject = entry as EntityRegistryEntry;
// Retrieve the device area ID only if expandToDevice is true
if (expandToDevice && entryObject.device_id) {
deviceAreaId = Registry.devices.find((device) => device.id === entryObject.device_id)?.area_id;
}
// Logic for 'undisclosed' areaId
if (areaId === 'undisclosed') {
return entry.area_id === areaId && (deviceAreaId === areaId || deviceAreaId === undefined);
if (areaId === 'undisclosed' || areaId === undefined) {
return entry.area_id === areaId && (!expandToDevice || deviceAreaId === areaId);
}
// Logic for undefined areaId
if (areaId === undefined) {
return entry.area_id === undefined && (!expandToDevice || deviceAreaId === undefined);
}
// Logic for any other areaId
return entry.area_id === areaId || (expandToDevice && deviceAreaId === areaId);
};
@@ -419,18 +406,6 @@ class RegistryFilter<T extends RegistryEntry, K extends keyof T = keyof T> {
return fork.entries.filter((entry, index) => fork.filters.every((filter) => filter(entry, index)));
}
/**
* Retrieves an array of values for a specified property from the filtered entries.
*
* @param {keyof T} propertyName - The name of the property whose values are to be retrieved.
* @returns {Array<T[keyof T]>} An array of values corresponding to the specified property.
* If the property does not exist in any entry, those entries will be filtered out.
*/
getValuesByProperty(propertyName: keyof T): Array<T[keyof T]> {
const entries = this.toList(); // Call toList to get the full entries
return entries.map((entry) => entry[propertyName]).filter((value) => value !== undefined) as Array<T[keyof T]>;
}
/**
* Applies all the accumulated filters to the entries and returns the first remaining entry.
*
@@ -466,7 +441,7 @@ class RegistryFilter<T extends RegistryEntry, K extends keyof T = keyof T> {
return result[0];
}
logMessage(lvlWarn, `Expected a single element, but found ${result.length}.`);
logMessage(lvlDebug, `Expected a single element, but found ${result.length}.`);
return undefined;
}

View File

@@ -19,7 +19,6 @@ export enum DebugLevel {
Fatal = 5,
}
// noinspection JSUnusedGlobalSymbols
/**
* Individually exported log level constants.
*
@@ -41,49 +40,7 @@ export const {
*
* @default DebugLevel.Off
*/
let currentLevel: DebugLevel = DebugLevel.Fatal;
/**
* Extracts the name of the function or method that called the logger from a stack trace string.
*
* Handles both Chrome and Firefox stack trace formats:
* - Chrome: "at ClassName.methodName (url:line:column)"
* - Firefox: "methodName@url:line:column"
*
* Returns the full caller (including class, if available), or "unknown" if not found.
*
* @param stack - The stack trace string, typically from new Error().stack
* @returns The caller's function/method name (with class if available), or "unknown"
*/
function getCallerName(stack?: string): string {
if (!stack) {
return 'unknown';
}
const lines = stack.split('\n').filter(Boolean);
// Find the first line that contains '@' and is not logMessage itself
for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim();
if (line.includes('@') && !line.startsWith('logMessage')) {
return line.split('@')[0] || 'anonymous';
}
// Fallback for anonymous functions
if (line.startsWith('@')) {
return 'anonymous function';
}
}
// Chrome fallback
for (let i = 1; i < lines.length; i++) {
const match = lines[i].match(/at ([^( ]+)/);
if (match && match[1] && match[1] !== 'logMessage') {
return match[1];
}
}
return 'unknown function';
}
let currentLevel: DebugLevel = DebugLevel.Off;
/**
* Sets the global log level.
@@ -104,11 +61,7 @@ export function setDebugLevel(level: DebugLevel) {
* @param {string} message - The message to log.
* @param {unknown[]} [details] - Optional extra details (e.g., error object).
*
* @throws {Error} After logging, if the level is `lvlError` or `lvlFatal`.
*
* @remarks
* It might be required to throw an additional Error after logging with `lvlError ` or `lvlFatal` to satify the
* TypeScript compiler.
* @throws {Error} After logging, if the level is `Fatal` or `Error`.
*/
export function logMessage(level: DebugLevel, message: string, ...details: unknown[]): void {
if (currentLevel === DebugLevel.Off || level > currentLevel) {
@@ -118,23 +71,22 @@ export function logMessage(level: DebugLevel, message: string, ...details: unkno
const frontEndMessage: string = 'Mushroom Strategy - An error occurred. Check the console (F12) for details.';
const prefix = `[${DebugLevel[level].toUpperCase()}]`;
const safeDetails = details.map(deepClone);
const caller = `[at ${getCallerName(new Error().stack)}]`;
switch (level) {
case DebugLevel.Debug:
console.debug(`${prefix}${caller} ${message}`, ...safeDetails);
console.debug(`${prefix} ${message}`, ...safeDetails);
break;
case DebugLevel.Info:
console.info(`${prefix}${caller} ${message}`, ...safeDetails);
console.info(`${prefix} ${message}`, ...safeDetails);
break;
case DebugLevel.Warn:
console.warn(`${prefix}${caller} ${message}`, ...safeDetails);
console.warn(`${prefix} ${message}`, ...safeDetails);
break;
case DebugLevel.Error:
console.error(`${prefix}${caller} ${message}`, ...safeDetails);
console.error(`${prefix} ${message}`, ...safeDetails);
throw frontEndMessage;
case DebugLevel.Fatal:
console.error(`${prefix}${caller} ${message}`, ...safeDetails);
console.error(`${prefix} ${message}`, ...safeDetails);
alert?.(`${prefix} ${message}`);
throw frontEndMessage;
}

View File

@@ -62,6 +62,7 @@ export default function setupCustomLocalize(hass?: HomeAssistant): void {
/**
* Translate a key using the globally configured localize function.
* Throws if not initialized.
*/
export function localize(key: string): string {
if (!_localize) {