import { Logger } from 'winston'; import { Driver } from './Definitions/Global/Drivers.js'; import { Family } from './Definitions/Global/Families.js'; import { UnitOfMeasure } from './Definitions/Global/UOM.js'; import { type Message, Feature, ISY } from './ISY.js'; import type { Merge, UnionToIntersection } from '@matter/general'; import { CliConfigSetLevels } from 'winston/lib/winston/config/index.js'; import { Converter } from './Converters.js'; import type { Command } from './Definitions/Global/Commands.js'; import { Event } from './Definitions/Global/Events.js'; import type { CompositeDevice } from './Devices/CompositeDevice.js'; import { NodeFactory } from './Devices/NodeFactory.js'; import type { DriverState } from './Model/DriverState.js'; import { NodeInfo } from './Model/NodeInfo.js'; import type { NodeNotes } from './Model/NodeNotes.js'; import { NodeType } from './NodeType.js'; import type { ISYScene } from './Scenes/ISYScene.js'; import type { Factory as BaseFactory, MaybeWithUOM } from './Utils.js'; import { type ObjectToUnion, type StringKeys } from './Utils.js'; import type { Jsonify } from 'type-fest'; import type { JsonifyObject } from 'type-fest/source/jsonify.js'; import type { EventEmitter } from 'stream'; //type DriverValues = {[x in DK]?:V}; export class ISYNode< T extends Family = Family, D extends ISYNode.DriverSignatures = {}, C extends ISYNode.CommandSignatures = {}, E extends ISYNode.EventSignatures = {}, > { // #region Properties (32) static #displayNameFunction: Function; #parentNode: ISYNode; public readonly address: string; public readonly baseLabel: string; public readonly flag: any; public readonly isy: ISY; public readonly nodeDefId: string; public static family: Family; public static nodeDefId = 'Unknown'; public static implements: string[] = []; public commands: Command.ForAll; //public readonly formatted: DriverValues = {}; //public readonly uom: { [x in Driver.Literal]?: UnitOfMeasure } = { ST: UnitOfMeasure.Boolean }; //public readonly pending: DriverValues = {}; //public readonly local: DriverValues = {}; public drivers: Driver.ForAll = {} as Driver.ForAll; public enabled: boolean; //TODO: add signature for non-command/non-driver events public events: Event.EventEmitter; //Event.FunctionSigFor> & Omit /*{ [x in E]: x extends keyof D ? {name:`${D[x]["name"]}Changed`, driver: x, value: D[x]["value"], formatted: string, uom: UnitOfMeasure} : x extends keyof C ? {name: `${C[x]['name']}Triggered`, command: x} : {name: E}; };*/ public family: T; public folder: string = ''; public hidden: boolean; public isDimmable: boolean; public isLoad: boolean; public label: string; public lastChanged: Date; public location: string; public logger: (msg: any, level?: keyof CliConfigSetLevels, ...meta: any[]) => Logger; // [x: string]: any; public name: string; public nodeType: number; public parent: any; public parentAddress: any; public parentType: NodeType; public propsInitialized: boolean; public scenes: ISYScene[]; public spokenName: string; public type: any; public features: Feature; // #endregion Properties (32) // #region Constructors (1) constructor(isy: ISY, node: NodeInfo) { this.isy = isy; this.isy.nodes.set(node.address, this); this.nodeType = 0; this.flag = node.flag; this.nodeDefId = node.nodeDefId; this.address = String(node.address); this.name = node.name; this.family = node.family as T; this.parent = node.parent; this.parentType = Number(this.parent?.type); this.enabled = node.enabled ?? true; this.propsInitialized = false; const s = this.name.split('.'); //if (s.length > 1) //s.shift(); this.baseLabel = s .join(' ') .replace(/([A-Z])/g, ' $1') .replace(' ', ' ') .replace(' ', ' ') .trim(); if (this.parentType === NodeType.Folder) { this.folder = isy.folderMap.get(this.parent._); isy.logger.debug(`${this.name} is in folder ${this.folder}`); this.logger = (msg: any, level: keyof CliConfigSetLevels = 'debug', ...meta: any[]) => { isy.logger[level](`${this.folder} ${this.name} (${this.address}): ${msg}`, meta); return isy.logger; }; this.label = `${this.folder} ${this.baseLabel}`; } else { this.label = this.baseLabel; this.logger = (msg: any, level: keyof CliConfigSetLevels = 'debug', ...meta: any[]) => { isy.logger[level](`${this.name} (${this.address}): ${msg}`, meta); return isy.logger; }; } this.events = new Event.NodeEventEmitter(this) as Event.EventEmitter; this.lastChanged = new Date(); } // #endregion Constructors (1) // #region Public Getters And Setters (1) public get parentNode(): ISYNode { if (this.#parentNode === undefined) { if (this.parentAddress !== this.address && this.parentAddress !== null && this.parentAddress !== undefined) { this.#parentNode = this.isy.getDevice(this.parentAddress) as unknown as ISYNode; if (this.#parentNode !== null) { //this.#parentNode.addChild(this); } } this.#parentNode = null; } return this.#parentNode; } // #endregion Public Getters And Setters (1) // #region Public Methods (18) public addLink(isyScene: ISYScene) { this.scenes.push(isyScene); } _initialized: boolean = false; public get initialized(): boolean { if (!this._initialized) { for (const prop in this.drivers) if (this.drivers[prop]?.initialized == false && prop != 'ERR') return false; } this._initialized = true; return true; } public applyStatus(prop: DriverState) { try { var d = this.drivers?.[prop.id]; if (d) { d.apply(prop); this.logger(`Property ${d?.label ?? prop.id} (${prop.id}) refreshed to: ${d.value} (${prop.formatted}})`); //d.state.value = this.convertFrom(prop.value, prop.uom, prop.id); //d.state.formatted = prop.formatted; //d.state.uom = prop.uom; } else { this.logger(`Driver ${prop.id} not found, creating new driver`); //@ts-expect-error this.drivers[prop.id] = Driver.create(prop.id as never, this as any, prop, { uom: prop.uom, label: (prop.name as string) ?? (prop.id as string), name: (prop.name as string) ?? (prop.id as string), }); } } catch (e) { this.logger(e?.message ?? e, 'error'); } } public convert(value: any, from: UnitOfMeasure, to: UnitOfMeasure): any { if (from === to) return value; else { try { return Converter.Standard[from][to].from(value); } catch { this.isy.logger.error(`Conversion from ${UnitOfMeasure[from]} to ${UnitOfMeasure[to]} not supported.`); } finally { return value; } } } public convertFrom(value: any, uom: UnitOfMeasure, propertyName?: StringKeys): any { if (this.drivers[propertyName]?.uom != uom) { this.logger( `Converting ${this.drivers[propertyName].label} to ${UnitOfMeasure[this.drivers[propertyName]?.uom]} from ${UnitOfMeasure[uom]}` ); return this.convert(value, uom, this.drivers[propertyName].uom); } } public convertTo(value: any, uom: UnitOfMeasure, propertyName?: StringKeys) { if (this.drivers[propertyName]?.uom != uom) { this.isy.logger.debug( `Converting ${this.drivers[propertyName].label} from ${UnitOfMeasure[this.drivers[propertyName].uom]} to ${UnitOfMeasure[uom]}` ); return this.convert(value, uom, this.drivers[propertyName].uom); } } public emit( event: 'propertyChanged' | 'controlTriggered', propertyName?: string, newValue?: any, oldValue?: any, formattedValue?: string, controlName?: string ) { //if ('PropertyChanged') return super.emit(event, propertyName, newValue, oldValue, formattedValue); //else if ('ControlTriggered') return super.emit(event, controlName); } public generateLabel(template: string): string { // tslint:disable-next-line: only-arrow-functions if (!ISYNode.#displayNameFunction) { // template = template.replace("{", "{this."}; const regex = /(?\w+) \?\? (?\w+)/g; this.logger(`Display name format: ${template}`); let newttemp = template.replace( regex, "this.$ === null || this.$ === undefined || this.$ === '' ? this.$ : this.$" ); this.logger(`Template format updated to: ${newttemp}`); const s = { location: this.location ?? '', folder: this.folder ?? '', spokenName: this.spokenName ?? this.name, name: this.name ?? '', }; newttemp = newttemp.replace('this.name', 'this.baseLabel'); ISYNode.#displayNameFunction = new Function(`return \`${newttemp}\`.trim();`); } return ISYNode.#displayNameFunction.call(this); } public async getNotes(): Promise { try { const result = await this.isy.sendRequest(`nodes/${this.address}/notes`, { trailingSlash: false, errorLogLevel: 'silly', validateStatus(status) { return true; }, }); if (result !== null && result !== undefined) { return result.NodeProperties; } else { return null; } } catch (e) { return null; } } public handleControlTrigger(controlName: keyof E & keyof C): boolean { //this.lastChanged = new Date(); //this.events.emit(`${this.commands[controlName].name}`, controlName); return true; } public handleEvent(event: Message): boolean { let actionValue = null, formattedValue = null, uom = null, prec = null; if (event.action instanceof Object) { actionValue = event.action._; uom = event.action.uom; prec = event.action.prec; } else if (typeof event.action == 'number' || typeof event.action == 'string') { actionValue = Number(event.action); } if (event.control in this.drivers) { // property not command formattedValue = 'fmtAct' in event ? event.fmtAct : actionValue; return this.handlePropertyChange( event.control as StringKeys, actionValue, uom ?? UnitOfMeasure.Unknown, prec, formattedValue ); } else if (event.control === '_3') { this.logger(`Received Node Change Event: ${JSON.stringify(event)}.`, 'debug'); } else { // this.logger(event.control); const e = event.control; const dispName = this.commands?.[e]?.name; if (dispName !== undefined && dispName !== null) { this.logger(`Command ${dispName} (${e}) event received.`); } else { this.logger(`Command ${e} event received.`); } this.handleControlTrigger(e); return true; } } public handlePropertyChange( propertyName: StringKeys, value: any, uom: UnitOfMeasure, prec?: number, formattedValue?: string ): boolean { this.lastChanged = new Date(); let driver = this.drivers?.[propertyName]; /*this.logger(`Driver ${propertyName} (${driver?.label} value update ${value} (${formattedValue}) uom: ${UnitOfMeasure[uom]} event received.`);*/ if (driver?.patch(value, formattedValue, uom, prec)) { //this.emit('propertyChanged', propertyName, value, oldValue, formattedValue); this.scenes?.forEach(element => { this.logger(`Recalulating ${element.deviceFriendlyName}`); element.recalculateState(); }); } return true; } /*public override on(event: 'PropertyChanged', listener: (propertyName: keyof D, newValue: any, oldValue: any, formattedValue: string) => any): this; public override on(event: 'ControlTriggered', listener: (controlName: keyof C) => any): this; public override on(event: string | symbol, listener: (...args: any[]) => void): this { super.on(event, listener); return this; }*/ public parseResult(node: { property: DriverState | DriverState[] }) { if (Array.isArray(node.property)) { for (const prop of node.property) { this.applyStatus(prop); } } else if (node.property) { this.applyStatus(node.property); //device.local[node.property.id] = node.property.value; //device.formatted[node.property.id] = node.property.formatted; //device.uom[node.property.id] = node.property.uom; } } public async readProperties(): Promise { var result = await this.isy.sendRequest(`nodes/${this.address}/status`); this.logger(JSON.stringify(result), 'debug'); return result.property; } /*public addChild>(childDevice: K) { this.children.push(childDevice); }*/ public async readProperty(propertyName: keyof D & string): Promise { var result = await this.isy.sendRequest(`nodes/${this.address}/${propertyName}`); return result.property; } public async refreshState(): Promise { const device = this; const node = (await this.isy.sendRequest(`nodes/${this.address}/status`)).node; // this.logger(node); this.parseResult(node); return node; } public async refreshNotes() { const that = this; try { const result = await this.getNotes(); if (result !== null && result !== undefined) { that.location = result.location ?? this.folder ?? ''; that.spokenName = result.spoken ?? this.name ?? ''; if (result.isLoad) this.features &= Feature.HasLoad; // if(result.spoken) } else { //that.logger('No notes found.','debug'); } that.label = that.generateLabel.bind(that)(that.isy.displayNameFormat); that.label = that.label ?? this.baseLabel; that.logger(`The friendly name updated to: ${that.label}`); } catch (e) { that.logger(e); } } public async sendCommand(command: StringKeys): Promise; public async sendCommand( command: StringKeys, value: MaybeWithUOM, parameters: Record ): Promise; public async sendCommand(command: StringKeys, value: MaybeWithUOM): Promise; public async sendCommand( command: StringKeys, parameters: Record ): Promise; async sendCommand( command: StringKeys, valueOrParameters?: MaybeWithUOM | Record, parameters?: Record ): Promise { if (valueOrParameters === null || valueOrParameters === undefined) { return this.isy.sendNodeCommand(this, command); } if ( typeof valueOrParameters === 'string' || typeof valueOrParameters === 'number' || typeof valueOrParameters === 'boolean' || Array.isArray(valueOrParameters) ) { return this.isy.sendNodeCommand(this, command, valueOrParameters, parameters); } if (typeof valueOrParameters === 'object' && !Array.isArray(valueOrParameters)) { return this.isy.sendNodeCommand(this, command, null, { ...valueOrParameters, ...parameters }); } ///return this.isy.sendNodeCommand(this, command, valueOrParameters, { ...parameters }); } public async updateProperty(propertyName: string, value: any): Promise { var l = this.drivers[propertyName]; if (l) { if (l.serverUom) l.state.pendingValue = this.convert(value, l.uom, l.serverUom); else l.state.pendingValue = value; } this.logger(`Updating property ${l.label}. incoming value: ${value} outgoing value: ${l.state.pendingValue}`); return this.isy.sendRequest(`nodes/${this.address}/set/${propertyName}/${l.state.pendingValue}`).then(p => { l.state.pendingValue = null; }); } toJSON() { return { ...this, initialized: this.initialized, family: Family[this.family], type: NodeType[this.type], scenes: this.scenes?.map(s => { return { address: s.address, name: s.name }; }), commands: this.commands ? Object.entries(this.commands).map(([id, c]) => { return { id: id, name: c.name, }; }) : [], isy: undefined, parent: this.parent ? { address: this.parent.address, name: this.parent.name } : undefined, }; } // #endregion Public Methods (18) } export type Flatten = UnionToIntersection< T extends Record ? K extends string ? T[K] extends Record ? keyof T[K] extends string ? { [x in `${K}.${keyof T[K]}`]: T[K][TakeLast] } : never : never : never : never >; type Split = X extends `${infer A}.${infer B}` ? [A, ...Split] : never; type TakeLast = X extends `${infer A}.${infer B}` ? TakeLast : X; type Test = Flatten<{ a: { b: { c: string } } }>; export type DriverMap = Flatten<{ [x in keyof T]: DriversOf }>; export type NodeList = { [x: string]: ISYNode }; export type DriversOf = T extends ISYNode ? D : never; export type CommandsOf = T extends ISYNode ? C : never; export type EventsOf = T extends ISYNode ? E : never; export namespace ISYNode { export type FromSignatures = T extends DriverSignatures ? Driver.ForAll : never; export interface Factory = ISYNode> extends BaseFactory { Commands; Drivers; } type InternalDriversOf = T extends ISYNode ? D : never; export type DriversOf = T extends ISYNode ? InternalDriversOf : T extends CompositeDevice ? T['drivers'] : never; export type CommandsOf = T extends ISYNode ? T['commands'] : never; export type EventsOf = T extends ISYNode ? E : never; export type FamilyOf = T extends ISYNode ? F : never; export type DriverTypesOf = ObjectToUnion>; export type CommandTypesOf = ObjectToUnion>; export type EventTypesOf = ObjectToUnion>; export type EventNamesOf = EventTypesOf extends { name: infer U } ? U : never; export type DriverNamesOf = T extends { Drivers } ? keyof T['Drivers'] : DriverTypesOf extends { name: infer U } ? U : DriversOf extends { name: infer U } ? U : never; export type DriverKeysOf = keyof DriversOf; export type CommandKeysOf = keyof CommandsOf; export type CommandNamesOf = T extends { Commands } ? keyof T['Commands'] : never; export type List = NodeList; export type DriverMap = Flatten<{ [x in keyof T]: DriversOf; }>; export type CommandMap = Flatten<{ [x in keyof T]: CommandsOf; }>; export type EventMap = Flatten<{ [x in keyof T]: EventsOf; }>; export type DriverSignatures = Record>; export type CommandSignatures = Partial<{ [x: string]: Command.Signature; }>; export type EventSignatures = Record; export function getImplements(node: ISYNode | typeof ISYNode): string[] { return NodeFactory.getImplements(node); } //TODO: fix return types /*export type WithCommands> = C extends Command.Signatures ? { [K in C[U]["name"]]: C[K]; } : never;*/ /*export const With = >>(base: T, drivers: D, commands: C) => { return class extends base implements Omit, 'events'> { declare drivers: Driver.ForAll; declare commands: Command.ForAll; }; };*/ export type WithDrivers = D extends Driver.Signatures ? { [K in D[U] as K['name']]: K['value']; } : never; }