isy-nodejs
Version:
Node.js wrapper for ISY interface including websockets for change notifications. Fork of isy-js by Rod Toll. Designed to be used in a node.js application.
566 lines (464 loc) • 16.4 kB
text/typescript
import * as log4js from '@log4js-node/log4js-api';
import winston, { Logger, format, type LeveledLogMethod } from 'winston';
import { EventEmitter as BaseEventEmitter } from 'events';
//import { get } from 'http';}
import type { PackageJson } from '@npmcli/package-json';
import { type AxiosRequestConfig } from 'axios';
import chalk, { type ChalkInstance } from 'chalk';
import { existsSync, mkdirSync } from 'fs';
import { writeFile as baseWriteFile, readFile } from 'fs/promises';
import { resolve } from 'import-meta-resolve';
import { merge } from 'moderndash';
import Mutex, * as mutex from 'p-mutex';
import path from 'path';
import {
Constructor,
Primitive,
type Get,
type JsonObject,
type JsonPrimitive,
type Merge,
type SnakeCase,
type SnakeCasedProperties,
type SnakeCasedPropertiesDeep,
type StringKeyOf,
} from 'type-fest';
import { fileURLToPath } from 'url';
import { EventType } from './Events/EventType.js';
import type { ISY, UnitOfMeasure, Family } from './ISY.js';
import type { ISYNode } from './ISYNode.js';
import type { RelayLamp } from './Devices/Insteon/index.js';
export interface Factory<T extends object> {
create?(...args: any[]): Promise<T> | T;
is?<K extends T>(obj: object): obj is T;
isImplementedBy?(obj: object): obj is T;
Class: Constructor<T>;
}
export function isFactory(obj: any): obj is Factory<any> {
return obj.Class !== undefined;
}
export function includes<T>(value: T, maybeArray: MaybeArray<T>): boolean;
export function includes<T>(value: T, ...maybeArray: T[]): boolean {
if (Array.isArray(maybeArray)) {
return maybeArray.includes(value);
} else if (maybeArray != null) {
return maybeArray === value;
} else {
return false;
}
}
export async function writeFile<T extends JsonObject | string>(
data: T,
filename: string,
...paths: string[]
): Promise<void> {
try {
let o = path.resolve(...paths);
if (!existsSync(o)) {
mkdirSync(o, { recursive: true });
}
return await baseWriteFile(path.resolve(o, filename), typeof data == 'object' ? JSON.stringify(data) : data);
} catch (err) {
console.error(`Error writing file ${filename} to folder: ${err}`, err);
}
}
export async function writeDebugFile<T extends JsonObject | string>(
data: T,
filename: string,
logger?: winston.Logger,
...paths: string[]
): Promise<void> {
try {
let o = path.resolve(...paths, 'debug');
if (!existsSync(o)) {
mkdirSync(o, { recursive: true });
}
return await baseWriteFile(path.resolve(o, filename), typeof data == 'object' ? JSON.stringify(data) : data);
} catch (err) {
logger?.error(`Error writing file ${filename} to debug folder: ${err}`, err);
}
}
export function some<T>(predicate: (p: T) => boolean, ...maybeArray: T[]): boolean {
return maybeArray.some(predicate);
}
export type WithUOM<P extends Primitive = JsonPrimitive, U extends UnitOfMeasure = UnitOfMeasure> = [value: P, uom: U];
export type MaybeWithUOM<P extends Primitive = JsonPrimitive, U extends UnitOfMeasure = UnitOfMeasure> =
| [value: P | undefined, uom: U]
| P;
export function hasUOM<P extends Primitive>(value: MaybeWithUOM<P>): value is WithUOM<P>;
export function hasUOM<P extends Primitive, U extends UnitOfMeasure>(
value: MaybeWithUOM<P, U>,
uom?: U
): value is WithUOM<P, U> {
if (!Array.isArray(value)) {
return false;
} else if (uom != null) return value['uom'] == uom;
else return true;
}
export function isPrimitive(value: any): value is JsonPrimitive {
return typeof value == 'string' || typeof value == 'boolean' || typeof value == 'number';
}
export function getConstructor<T extends object>(obj: Factory<T> | Constructor<T>): Constructor<T> {
if (isFactory(obj)) {
return obj.Class;
}
return obj;
}
export type ConstructorOf<T> =
T extends Factory<infer U> ? Constructor<U> : T extends Constructor<infer U> ? Constructor<U> : Constructor<T>;
export type InstanceOf<T> = T extends Factory<infer U> ? U : T extends Constructor<infer U> ? U : T;
export type ExtractKeys<T, U> = keyof { [K in keyof T]: T[K] extends U ? K : never };
export type StringKeys<T> = Extract<keyof T, string>;
export type PickOfType<T, U> = {
[K in keyof T]: T[K] extends U ? (any extends T[K] ? never : K) : undefined;
}[keyof T];
export type Paths<T> = T extends object
? { [K in keyof T]: `${Exclude<K, symbol>}${'' | `.${Paths<T[K]>}`}` }[keyof T]
: never;
export type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
export type Join<K, P> = K extends string | number
? P extends string | number
? `${K}${'' extends P ? '' : '.'}${P}`
: never
: never;
export type PathsWithLimit<T, D extends number = 10> = [D] extends [never]
? never
: T extends object
? {
[K in keyof T]-?: K extends string | number ? `${K}` | Join<K, PathsWithLimit<T[K], Prev[D]>> : never;
}[keyof T]
: '';
export type Leaves<T, D extends number = 10> = [D] extends [never]
? never
: T extends object
? { [K in keyof T]-?: Join<K, Leaves<T[K], Prev[D]>> }[keyof T]
: '';
export function getEnumValueByEnumKey<E extends { [index: string]: number }, T extends keyof E>(
enumType: E,
enumKey: T
): E[T] {
return enumType[enumKey];
}
export type Replace<K extends string, T extends string, U extends string> = K extends `${infer L}${T}${infer R}`
? `${L}${U}${R}`
: K;
export type Remove<K extends string, T extends string> = K extends `${infer L}${T}${infer R}` ? `${L}${R}` : K;
export type ReplaceAll<K extends string, T extends string, U extends string> = K extends `${infer L}${T}${infer R}`
? `${L}${U}${ReplaceAll<R, T, U>}`
: K;
export function getEnumKeyByEnumValue<E extends { [index: string]: number }, T extends E[keyof E]>(
enumType: E,
enumValue: E[T]
): T {
return Object.keys(enumType).find(key => enumType[key] === enumValue) as unknown as T;
}
export namespace Enums {
export function getKeyByValue<E extends { [index: string]: number }, T extends E[keyof E]>(
enumType: E,
enumValue: E[T]
): T {
return Object.keys(enumType).find(key => enumType[key] === enumValue) as unknown as T;
}
export type ValuesOf<TEnum extends number | string | boolean | bigint> =
`${TEnum}` extends `${infer R extends number}` ? R : `${TEnum}`;
export type KeysOf<TEnum extends number | string | boolean | bigint> = `${TEnum}` extends infer R extends string
? R
: `${TEnum}`;
}
export function updateConfig<U extends Record<PropertyKey, any>, T extends U = U>(
baseConfig: T,
...updates: Partial<U>[]
): T {
return merge(baseConfig, updates[0], ...updates.slice(1)) as any as T;
}
export type LogLevel = PickOfType<winston.Logger, LeveledLogMethod>;
export type LabelFor<TEnum, TEnumMember> = PickOfType<TEnum, TEnumMember>;
export type IdentityOf<T> = T extends (...args: any[]) => infer R ? R : T;
export type MaybeArray<T> = T | T[];
export type ObjectToUnion<T> = T[keyof T];
export function toArray<T>(value: MaybeArray<T>): T[] {
if (undefined === value) return [];
return Array.isArray(value) ? value : [value];
}
export function fromArray<T>(...value: T[]): MaybeArray<T> {
if (value.length === 1) {
return value[0] ?? undefined;
}
return value;
}
export type BaseRequestConfig = Pick<AxiosRequestConfig, 'auth' | 'baseURL' | 'socketPath'>;
export type ISYRequestConfig = Omit<AxiosRequestConfig, keyof BaseRequestConfig | 'method'>;
export function byteToPct(value) {
return Math.round((value * 100) / 255);
}
export function pctToByte(value) {
return Math.round((value * 255) / 100);
}
export function byteToDegree(value) {
return Math.fround(value / 2);
}
let lastrequest = Promise.resolve();
declare module 'winston' {
interface Logger extends LoggerLike {}
}
declare global {
interface String {
// #region Public Methods (6)
left(numChars: number): string;
leftWithToken(numChars: number, token?: string): string;
remove(string: string): string;
removeAll(string: string): string;
right(numChars: number): string;
rightWithToken(numChars: number, token?: string): string;
// #endregion Public Methods (6)
}
}
export type ToENV<T> = SnakeCase<T>;
export type UpperCaseProperties<T extends Record<string, any>> = {
[K in StringKeys<T> as Uppercase<`${K}`>]: T[K];
};
export type PROCESS_ENV = {
[x in Uppercase<ReplaceAll<Leaves<SnakeCasedPropertiesDeep<{ isy: ISY.Config }>>, '.', '_'>>]: string;
};
export type AsENV<T extends Record<string, unknown>> = {
[x in Leaves<T> as Uppercase<ReplaceAll<SnakeCase<`${x}`>, '.', '_'>>]?: Get<T, x> | undefined;
};
export type ENVISYConfig = AsENV<{ isy: ISY.Config }>;
export interface LoggerLike extends Partial<log4js.Logger> {
//default(msg: any): void;
}
// `${`${const origCreateLogger = winston.createLogger.bind(winston)
// Logger.prototwinston.createLogger = (options) =>
// {
// let logger = winston.createLogger(options);
// logger.prototype = logger.log.bind(logger);
// }
// }`}`
export function valueOf<E, T extends Extract<keyof E, string>>(e: E, val: T): E[T] {
return e[val];
}
export function clone(logger: Logger, label: string): Logger {
return winston.createLogger({
format: format.label({ label }),
transports: logger.transports,
level: logger.level,
levels: logger.levels,
exitOnError: logger.exitOnError,
exceptionHandlers: logger.exceptions,
...logger,
});
// `${const copy1 = { ...logger };copy1.prefix = copy1.prefix = prefix ?? logger.prototype;
// const copy = logger.info.bind(copy1) as Logging;
// Object.assign(copy, logger);
// copy.prefix = prefix ?? logger.prefix;
// copy.isDebugEnabled = () => ISYPlatform.Instance.debugLoggingEnabled;
// copy.isErrorEnabled = () => true;
// copy.isWarnEnabled = () => true;
// copy.isFatalEnabled = () => true;
// copy.isTraceEnabled = () => true;
// // copy._log = logger._log.bind(copy);
// copy.debug = logger.debug.bind(copy);
// // copy.fatal = logger..bind(copy);
// copy.info = logger.info.bind(copy);
// copy.error = logger.error.bind(copy);
// copy.warn = logger.warn.bind(copy);
// copy.trace = ((message: ConcatArray<string>, ...args: any[]) => {
// // onst newMsg = chalk.dim(msg);
// if (copy.isTraceEnabled) {
// copy.log.apply(this, ['trace'].concat(message).concat(args));
// }
// }).bind(copy);
// copy.fatal = ((message: ConcatArray<string>, ...args: any[]) => {
// // onst newMsg = chalk.dim(msg);
// if (logger?.isFatalEnabled) {
// logger.log.apply(this, ['fatal'].concat(message).concat(args));
// }
// }).bind(copy);}`
//return copy;
}
// export function wire(logger: Logger) {
// logger.isDebugEnabled = () => ISYPlatform.Instance.debugLoggingEnabled;
// logger.isErrorEnabled = () => true;
// logger.isWarnEnabled = () => true;
// logger.isFatalEnabled = () => true;
// logger.isTraceEnabled = () => true;
// logger.trace = ((message, ...args: any[]) => {
// // onst newMsg = chalk.dim(msg);
// if (logger.isTraceEnabled()) {
// logger.log.apply(this, ['trace'].concat(message).concat(args));
// }
// }).bind(logger);
// logger.fatal = ((message, ...args: any[]) => {
// // onst newMsg = chalk.dim(msg);
// if (logger.isFatalEnabled()) {
// logger.log.apply(this, ['fatal'].concat(message).concat(args));
// }
// }).bind(logger);
// }
type TEventType = keyof typeof EventType;
export interface PropertyChangedEventEmitter extends EventEmitter<'PropertyChanged'> {
// #region Public Methods (1)
on(
event: 'PropertyChanged',
listener: (propertyName: string, newValue: any, oldValue: any, formattedValue: string) => void
): this;
// #endregion Public Methods (1)
}
export class EventEmitter<T extends TEventType> extends BaseEventEmitter {
// #region Public Methods (1)
public override on(
event: T,
listener:
| ((propertyName: string, newValue: any, oldValue: any, formattedValue: string) => any)
| ((controlName: string) => any)
): this {
return super.on(event, listener);
}
// #endregion Public Methods (1)
}
export function right(this: string, numChars: number) {
var l = this.length;
return this.substring(l - numChars);
}
export function left(this: string, numChars: number) {
return this.substring(0, numChars - 1);
}
export function rightWithToken(this: string, maxNumChars: number, token: string = ' ') {
var s = this.split(token);
var sb = s.pop();
var sp = s.pop();
while (sp !== undefined && sb.length + sp.length + token.length <= maxNumChars) {
sb = sp + token + sb;
sp = s.pop();
}
}
export function leftWithToken(this: string, maxNumChars: number, token: string = ' ') {
var s = this.split(token).reverse();
var sb = s.pop();
var sp = s.pop();
while (sp !== undefined && sb.length + sp?.length + token.length <= maxNumChars) {
sb = sb + token + sp;
sp = s.pop();
}
}
export function remove(this: string, searchValue: string | RegExp) {
return this.replace(searchValue, '');
}
export function removeAll(this: string, searchValue: string | RegExp) {
return this.replaceAll(searchValue, '');
}
const closeMutex = new Mutex();
const exitMutex = new Mutex();
export async function disposeGracefully(object: Disposable | AsyncDisposable, logger?: winston.Logger): Promise<void> {
await closeMutex.withLock(async () => {
if (Symbol.dispose in object) {
logger?.warn(`Disposing ${object.constructor.name}`);
object[Symbol.dispose]();
logger?.warn(`Disposed ${object.constructor.name}`);
} else if (Symbol.asyncDispose in object) {
logger?.warn(`Async disposing ${object.constructor.name}`);
await object[Symbol.asyncDispose]();
logger?.warn(`Async disposed ${object.constructor.name}`);
}
});
}
export async function exitGracefully(exitCode: number, logger?: winston.Logger): Promise<void> {
await exitMutex.withLock(async () => {
logger.once('finish', () => {
process.exit(exitCode);
});
if (exitCode === 0) {
logger?.info('Exiting');
} else {
logger?.error(`Exiting with error code ${exitCode}`);
}
logger.end();
});
}
/*export function parseTypeCode(typeCode: `${string}.${string}.${string}.${string}`): { category: Category; deviceCode: number; firmwareVersion: number; minorVersion: number } {
try {
const s = typeCode.split('.');
let output = { category: Number(s[0]), deviceCode: Number(s[1]), firmwareVersion: Number(Number(s[2]).toString(16)), minorVersion: Number(Number(s[3]).toString(16)) };
return output;
} catch (err) {
return null;
}
}*/
function getImportMeta() {
try {
//@ts-ignore
return import.meta;
} catch (err) {
//@ts-ignore
let { dirname, filename } = { dirname: __dirname, filename: __filename };
return { dirname, filename };
}
}
export async function findPackageJson(moduleName: string = 'isy-nodejs'): Promise<PackageJson & { path: string }> {
let currentPath = fileURLToPath(resolve(moduleName, import.meta.url));
console.log(currentPath);
while (currentPath !== '/') {
const packageJsonPath = path.join(currentPath, 'package.json');
if (existsSync(packageJsonPath)) {
let s = JSON.parse((await readFile(packageJsonPath)).toString());
if (s.name?.toLowerCase() === moduleName.toLowerCase()) {
return { ...s, path: packageJsonPath };
}
}
currentPath = path.resolve(currentPath, '..');
}
//} catch {
// return (await import('../../package.json', { with: { type: 'json' } })).default;
//}
//return null;
}
export function emphasize(text: unknown, color: ChalkInstance = chalk.white): string {
return color.bold(text);
}
export function logStringify(obj: any, indent = 2) {
let cache = [];
const retVal = JSON.stringify(
obj,
(key, value) => {
if (typeof value === 'object' && value !== null) {
if (cache.includes(value)) {
// Circular reference found, discard key
return;
}
// Store value in our collection
cache.push(value);
}
if (value instanceof Map) {
return [...value];
}
if (value instanceof Set) {
return [...value];
}
if (
key.toLowerCase().includes('password') ||
key.toLowerCase().includes('secret') ||
key.toLowerCase().includes('token') ||
key.toLowerCase().includes('key') ||
key.toLowerCase().includes('credentials') ||
key.toLowerCase().includes('credential')
) {
return '********';
}
return value;
},
indent
);
cache = null;
return retVal;
}
export type RelaxTypes<V> = V extends number
? number
: V extends bigint
? bigint
: V extends object
? V extends (...args: any[]) => any
? V
: {
[K in keyof V]: RelaxTypes<V[K]>;
}
: V;