UNPKG

@pmouli/isy-matter-server

Version:

Service to expose an ISY device as a Matter Border router

1,002 lines (888 loc) 34 kB
import axios, { type AxiosRequestConfig } from 'axios'; import { EventEmitter } from 'events'; import { format, Logger, loggers } from 'winston'; import WebSocket from 'ws'; import { type ParserOptions, Parser } from 'xml2js'; import { parseBooleans, parseNumbers } from 'xml2js/lib/processors.js'; import { DeviceFactory } from './Devices/DeviceFactory.js'; import { ELKAlarmPanelDevice } from './Devices/Elk/ElkAlarmPanelDevice.js'; import { ElkAlarmSensorDevice } from './Devices/Elk/ElkAlarmSensorDevice.js'; import { Message } from './ISY.js'; import { EventType } from './Events/EventType.js'; import { ISYDevice } from './ISYDevice.js'; import { ISYNode } from './ISYNode.js'; import { ISYVariable } from './ISYVariable.js'; import type { NodeInfo } from './Model/NodeInfo.js'; import { ISYScene } from './Scenes/ISYScene.js'; import { VariableType } from './VariableType.js'; import * as Utils from './Utils.js'; import { X2jOptions, XMLParser } from 'fast-xml-parser'; import type { ClientRequestArgs } from 'http'; import path from 'path'; import type { JsonPrimitive } from 'type-fest'; import { Family, UnitOfMeasure } from './Definitions/index.js'; import { GenericNode } from './Devices/GenericNode.js'; import { ISYError } from './ISYError.js'; import type { Config } from './Model/Config.js'; import { isPrimitive, writeDebugFile } from './Utils.js'; type InitStep = 'config' | 'loadNodes' | 'readFolders' | 'readDevices' | 'readScenes' | 'variables' | 'websocket' | 'refreshStatuses' | 'initialize'; class ISYInitializationError extends ISYError { step: InitStep; constructor(message: string, step: InitStep); constructor(error: Error, step: InitStep); constructor(messageOrError: string | Error, step: InitStep) { super(messageOrError as any); this.name = 'ISYInitializationError'; this.step = step; } } const defaultParserOptions: ParserOptions = { explicitArray: false, mergeAttrs: true, attrValueProcessors: [parseNumbers, parseBooleans], valueProcessors: [parseNumbers, parseBooleans], tagNameProcessors: [(tagName) => (tagName === 'st' || tagName === 'cmd' || tagName === 'nodeDef' ? '' : tagName)] }; const defaultXMLParserOptions: X2jOptions = { parseAttributeValue: true, allowBooleanAttributes: true, attributeNamePrefix: '', attributesGroupName: false, ignoreAttributes: false, // updateTag(tagName, jPath, attrs) { // //if(tagName === 'st' || tagName === 'cmd' || tagName === 'nodeDef') // // return false; // //return tagName; // }, textNodeName: '_', commentPropName: '$comment', cdataPropName: '$cdata', ignoreDeclaration: true, tagValueProcessor: (tagName, tagValue, jPath, hasAttributes, isLeafNode) => { if (tagValue === '') return null; return tagValue; }, isArray(tagName, jPath, isLeafNode, isAttribute) { if (tagName === 'property') return true; return false; } }; axios.defaults.transitional.forcedJSONParsing = false; const parser = new Parser(defaultParserOptions); export let Controls = {}; interface ISYConfig { // #region Properties (8) displayNameFormat?: string; elkEnabled?: boolean; enableWebSocket?: boolean; host: string; password: string; port: number; protocol: 'http' | 'https'; username: string; socketPath?: string; axiosOptions?: AxiosRequestConfig; webSocketOptions?: WebSocket.ClientOptions & ClientRequestArgs & { guardianInterval?: number }; // #endregion Properties (8) } export class ISY extends EventEmitter<{ nodeAdded: [ISYNode<any>]; initializeCompleted: [] }> implements Disposable, AsyncDisposable { // #region Properties (30) public readonly credentials: { username: string; password: string }; public readonly devices: Map<string, ISYDevice<any, any, any, any>> = new Map(); public readonly deviceMap: Map<string, string[]> = new Map(); public readonly displayNameFormat: string; public readonly elkEnabled: boolean; public readonly enableWebSocket: boolean; public readonly folderMap: Map<string, string> = new Map(); public readonly host: string; public readonly nodeMap: Map<string, ISYNode<any, any, any, any>> = new Map(); public readonly port: number; public readonly protocol: string; public readonly sceneList: Map<string, ISYScene> = new Map(); public readonly storagePath: string; public readonly variableList: Map<string, ISYVariable<VariableType>> = new Map(); public readonly wsprotocol: 'ws' | 'wss' = 'ws'; public readonly zoneMap: Map<string, ElkAlarmSensorDevice> = new Map(); public static instance: ISY; public configInfo: Config; public elkAlarmPanel: ELKAlarmPanelDevice; public guardianTimer: NodeJS.Timeout; public id: string; public lastActivity: any; public logger: Logger; public model: any; public nodesLoaded: boolean = false; public productId = 5226; public productName = 'eisy'; public firmwareVersion: any; public vendorName = 'Universal Devices, Inc.'; public webSocket: WebSocket; public apiVersion: string; socketPath: string; xmlParser: XMLParser; public axiosOptions: AxiosRequestConfig; guardianInterval: number; public static async create(config: ISYConfig, logger?: Logger, storagePath?: string): Promise<ISY> { if (!ISY.instance) { ISY.instance = new ISY(config, logger, storagePath); await ISY.instance.initialize(); } return ISY.instance; } // #endregion Properties (30) // #region Constructors (1) private constructor(config: ISYConfig, logger: Logger = new Logger(), storagePath?: string) { super(); this.enableWebSocket = config.enableWebSocket ?? true; this.storagePath = storagePath ?? './'; this.displayNameFormat = config.displayNameFormat ?? '${location ?? folder} ${spokenName ?? name}'; this.host = config.host; this.port = config.port; this.guardianInterval = config.webSocketOptions?.guardianInterval ?? 60000; this.credentials = { username: config.username, password: config.password }; this.protocol = config.protocol; this.wsprotocol = config.protocol === 'https' ? 'wss' : 'ws'; this.socketPath = config.socketPath; this.axiosOptions = { validateStatus: (status) => status >= 200 && status < 300 }; if (this.socketPath) { this.axiosOptions.socketPath = this.socketPath; this.axiosOptions.baseURL = 'http://dummy/'; //Workaround for Bug with Axios 1.7.5+ } else { (this.axiosOptions.baseURL = `${this.protocol}://${this.host}:${this.port}`), (this.axiosOptions.auth = { username: this.credentials.username, password: this.credentials.password }); } this.webSocketOptions = { origin: 'com.universal-devices.websockets.isy' }; if (this.socketPath) { this.webSocketOptions.socketPath = this.socketPath; } else { this.webSocketOptions.auth = `${this.credentials.username}:${this.credentials.password}`; } this.xmlParser = new XMLParser({ ...defaultXMLParserOptions }); //this.elkEnabled = config.elkEnabled ?? false; this.nodesLoaded = false; var fopts = format((info) => { info.message = JSON.stringify(info.message); return info; })({ label: 'ISY' }); this.logger = loggers.add('isy', { transports: logger.transports, levels: logger.levels, format: format.label({ label: 'ISY' }) }); this.guardianTimer = null; if (this.elkEnabled) { this.elkAlarmPanel = new ELKAlarmPanelDevice(this, 1); } ISY.instance = this; } [Symbol.dispose](): void { if (ISY.instance === this) { ISY.instance = null; } this.close(); this.logger.info('ISY instance disposed.'); } [Symbol.asyncDispose](): PromiseLike<void> { return new Promise((resolve) => { try { this.webSocket?.close(); this.logger.info('WebSocket connection closed during async dispose.'); } catch (e) { this.logger.error(`Error during async dispose: ${e.message}`); } finally { if (ISY.instance === this) { ISY.instance = null; } this.logger.info('ISY instance asynchronously disposed.'); resolve(); } }); } /*[Symbol.asyncDispose](): PromiseLike<void> { this.webSocket?.close(); return Promise.resolve(); }*/ // #endregion Constructors (1) // #region Public Getters And Setters (2) public get address() { return `${this.host}:${this.port}`; } public get isDebugEnabled() { return this.logger?.isDebugEnabled(); } // #endregion Public Getters And Setters (2) // #region Public Methods (24) /*[Symbol.dispose](): void { if (ISY.instance === this) { ISY.instance = null; } this.close(); }*/ /*public override emit(event: 'InitializeCompleted' | 'NodeAdded' | 'NodeRemoved' | 'NodeChanged', node?: ISYNode<any, any, any, any>): boolean { return super.emit(event, node); }*/ public getDevice<T extends ISYDevice<any, any, any, any> = ISYDevice<any, any, any, any>>(address: string, parentsOnly = false): T { let s = this.devices.get(address); if (!parentsOnly) { if (s === null) { s = this.devices[`${address.substr(0, address.length - 1)} 1`]; } } else { while (s.parentAddress !== undefined && s.parentAddress !== s.address && s.parentAddress !== null) { s = this.devices[s.parentAddress]; } } return s as T; } public getElkAlarmPanel() { return this.elkAlarmPanel; } public getNode<T extends ISYNode<any, any, any, any> = ISYNode<any, any, any, any>>(address: string, parentsOnly = false): T { let s = this.nodeMap.get(address); if (!parentsOnly) { if (s === null) { s = this.nodeMap[`${address.substr(0, address.length - 1)} 1`]; } } else { while (s.parentAddress !== undefined && s.parentAddress !== s.address && s.parentAddress !== null) { s = this.nodeMap[s.parentAddress]; } } return s as T; } public getScene(address: string) { return this.sceneList.get(address); } public getVariable<P extends VariableType>(type: P, id: number): ISYVariable<P> { const key = this.#createVariableKey(type, id); if (this.variableList.has(key)) { return this.variableList[key]; } return null; } public getVariableList() { return this.variableList; } public async handleInitializeError(step: string, reason: any): Promise<any> { this.logger.error(`Error initializing ISY (${step}): ${Utils.logStringify(reason)}`); return Promise.reject(reason); } public handleWebSocketMessage(event: { data: any }) { this.lastActivity = Date.now(); let that = this; parser.parseString(event.data, (err: any, res: { Event: Message<any> }) => { if (err) { that.logger.error(`Error parsing ISY WebSocket message: ${err} - ${event.data}`); return; } try { const evt = res?.Event; if (evt === undefined || evt.control === undefined) { return; } let actionValue = 0; if (evt.action instanceof Object) { actionValue = Number(evt.action._); } else if (typeof evt.action === 'string') { actionValue = Number(evt.action); } const stringControl = Number((evt.control as string)?.replace('_', '')); switch (stringControl) { case EventType.Elk: if (actionValue === 2) { this.elkAlarmPanel.handleEvent(event); } else if (actionValue === 3) { const zeElement = evt.eventInfo.ze; const zoneId = zeElement.zone; const zoneDevice = this.zoneMap[zoneId]; if (zoneDevice !== null) { if (zoneDevice.handleEvent(event)) { this.nodeChangedHandler(zoneDevice); } } } break; case EventType.Trigger: if (actionValue === 6) { const varNode = evt.eventInfo.var; const id = varNode.id; const type = varNode.type; this.getVariable(type, id)?.handleEvent(evt); } break; case EventType.Heartbeat: this.logger.debug(`Received ${EventType[EventType.Heartbeat]} Signal from ISY: ${JSON.stringify(evt)}`); break; case EventType.SystemStatusChanged: case EventType.ProgressReport: this.logger.silly(`${EventType[stringControl]} Message: ${JSON.stringify(evt)}`); break; default: if (evt.node !== '' && evt.node !== undefined && evt.node !== null) { // const impactedNode = this.getNode(evt.node); if (impactedNode !== undefined && impactedNode !== null) { try { impactedNode.handleEvent(evt); } catch (e) { this.logger.error(`Error handling message for ${impactedNode.name}: ${e.message}`); } } else { this.logger.silly(`${stringControl} Message for Unidentified Device: ${JSON.stringify(evt)}`); } } else { /*else if (stringControl === EventType.NodeChanged) { this.logger.debug(`Received Node Change Event: ${JSON.stringify(evt)}. These are currently unsupported.`);*/ this.logger.debug(`${EventType[stringControl]} Message: ${JSON.stringify(evt)}`); } break; } } catch (e) { this.logger.error(`Error handling WebSocket message: ${e.message} - ${JSON.stringify(event)}`); } }); } public async initialize(): Promise<boolean> { const that = this; try { await this.loadConfig(); await this.loadNodes(); await this.refreshStatuses(); await this.loadVariables(VariableType.Integer); await this.loadVariables(VariableType.State); await this.#finishInitialize(true); return true; } catch (e) { if (e instanceof ISYInitializationError) { if (e.step === 'variables') { this.logger.warn(`Error loading variables: ${e.message}`); await this.#finishInitialize(true); return true; } this.logger.error(`Error initializing ISY during (${e.step}): ${e.message}`); return false; } else { this.logger.error(`Error initializing ISY: ${e.message}`); return false; } } finally { if (this.nodesLoaded !== true) { await that.#finishInitialize(false); } } } webSocketOptions: WebSocket.ClientOptions & ClientRequestArgs; public async initializeWebSocket() { if (!this.enableWebSocket) { this.logger.warn('WebSocket is disabled. Skipping initialization.'); return; } try { const that = this; //const auth = `Basic ${Buffer.from(`${this.credentials.username}:${this.credentials.password}`).toString('base64')}`; let address = `${this.wsprotocol}://${this.address}/rest/subscribe`; if (this.socketPath) { address = `ws+unix:${this.socketPath}:/rest/subscribe`; } this.logger.info(`Opening webSocket: ${address}`); this.logger.info('Using the following websocket options: ' + JSON.stringify(this.webSocketOptions)); if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) { try { this.webSocket.close(); } catch (e) { this.logger.warn(`Error closing existing websocket: ${e.message}`); } } let p = new Promise<WebSocket>((resolve, reject) => { let webSocket = new WebSocket(`${address}`, ['ISYSUB'], this.webSocketOptions); //this.webSocket.onmessage = (event) => {this.handleWebSocketMessage() webSocket .on('open', () => { this.logger.info('Websocket connection open'); resolve(webSocket); }) .on('message', (data, b) => { that.logger.silly(`Received message: ${Utils.logStringify(data, 1)}`); that.handleWebSocketMessage({ data: data }); }) .on('error', (err: any, response: any) => { that.logger.warn(`Websocket subscription error: ${err}`); reject(new ISYInitializationError('Websocket subscription error', 'websocket')); }) .on('fail', (data: string, response: any) => { that.logger.warn(`Websocket subscription failure: ${data}`); reject(new Error('Websocket subscription failure')); }) .on('abort', () => { that.logger.warn('Websocket subscription aborted.'); throw new Error('Websocket subscription aborted.'); }) .on('timeout', (ms: string) => { that.logger.warn(`Websocket subscription timed out after ${ms} milliseconds.`); reject(new Error('Timeout contacting ISY')); //throw new Error('Timeout contacting ISY'); }) .on('close', (code: number, reason: string) => { that.logger.warn(`Websocket subscription closed: ${code} - ${reason}`); if (that.guardianTimer) { clearInterval(that.guardianTimer); that.guardianTimer = null; } that.webSocket = null; }); }); this.webSocket = await p; } catch (e) { throw new ISYInitializationError(e, 'websocket'); } } public async loadConfig(): Promise<any> { try { this.logger.info('Loading ISY Config'); const configuration = (await this.sendRequest('config')).configuration as Config; if (this.isDebugEnabled) { writeDebugFile(JSON.stringify(configuration), 'ISYConfig.json', this.logger, this.storagePath); } const controls = configuration.controls; this.model = configuration.deviceSpecs.model; this.firmwareVersion = configuration.app_full_version; this.vendorName = configuration.deviceSpecs.make; this.productId = configuration.product.id; this.productName = configuration.product.desc; this.id = configuration.root.id; // this.logger.info(result.configuration); if (controls !== undefined) { // this.logger.info(controls.control); // var arr = new Array(controls.control); for (const ctl of controls.control) { // this.logger.info(ctl); Controls[ctl.name] = ctl; } } return configuration; } catch (e) { throw new ISYInitializationError(e, 'config'); } } public async loadNodes(): Promise<any> { try { this.logger.info('Loading ISY Nodes'); const result = await this.sendRequest('nodes'); if (this.isDebugEnabled) await writeDebugFile(JSON.stringify(result), 'ISYNodes.json', this.logger, this.storagePath); await this.#readFolderNodes(result); await this.#readDeviceNodes(result); await this.#readSceneNodes(result); return result; } catch (e) { if (e instanceof ISYInitializationError) { throw e; } else { throw new ISYInitializationError(e, 'loadNodes'); } } } public async loadVariables(type: VariableType): Promise<any> { try { const that = this; this.logger.info(`Loading ISY Variables of type: ${type}`); let defs = await this.sendRequest(`vars/definitions/${type}`); this.#createVariables(type, defs); let vals = await this.sendRequest(`vars/get/${type}`); this.#setVariableValues(vals); } catch { this.logger.warn('error loading variables of type: ' + VariableType[type]); } } public nodeChangedHandler(node: ELKAlarmPanelDevice | ElkAlarmSensorDevice, propertyName = null) { const that = this; if (this.nodesLoaded) { // this.logger.info(`Node: ${node.address} changed`); // if (this.changeCallback !== undefined && this.changeCallback !== null) { // t//his.changeCallback(that, node, propertyName); // } } } /*public override on(event: 'initializeCompleted', listener: () => void): this; public override on(event: 'nodeAdded' | 'nodeRemoved' | 'nodeChanged', listener: (node?: ISYNode<any, any, any, any>) => void): this; override on(event: 'initializeCompleted' | 'nodeAdded' | 'nodeRemoved' | 'nodeChanged', listener: (node?: ISYNode<any, any, any, any>) => void): this { return super.on('nodeAdded',listener) }*/ public async refreshStatuses() { try { this.logger.info('Refreshing ISY Node Statuses'); const that = this; const result = await that.sendRequest('status'); if (that.isDebugEnabled) { await writeDebugFile(result, 'ISYStatus.json', this.logger, this.storagePath); } //this.logger.debug(result); for (const node of result.nodes.node) { let device = that.getNode(node.id) as unknown as ISYNode<any, any, any, any>; if (device !== null && device !== undefined) { device.parseResult(node); } } } catch (e) { throw new Error(`Error refreshing statuses: ${JSON.stringify(e.message)}`); } } public async sendGetVariable(id: any, type: any, handleResult: (arg0: number, arg1: number) => void) { const uriToUse = `vars/get/${type}/${id}`; return this.sendRequest(uriToUse).then((p) => handleResult(p.val, p.init)); } public async sendISYCommand(path: string): Promise<any> { // const uriToUse = `${this.protocol}://${this.address}/rest/${path}`; this.logger.info(`Sending command...${path}`); return this.sendRequest(path); } #formatParameter(name: string, value?: Utils.MaybeWithUOM<JsonPrimitive>): string { if (value === undefined) return ''; if (Utils.hasUOM(value)) { const [val, uom] = value; if (val === undefined || val === null) return ''; return `${name}.uom${uom}=${this.#formatParamValue(val)}`; } return `${name}=${this.#formatParamValue(value)}`; } #formatDefaultParameter(value: Utils.MaybeWithUOM<JsonPrimitive>): string { if (value === undefined) return ''; if (Utils.hasUOM(value)) { const [val, uom] = value; if (val === undefined || val === null) return ''; return `${this.#formatParamValue(val, uom)}/${uom}`; } return `${this.#formatParamValue(value)}`; } #formatParamValue(param: JsonPrimitive, uom?: UnitOfMeasure): string { if (typeof param === 'string') return param; if (typeof param === 'number') return param.toString(); if (typeof param === 'boolean') { if (uom == UnitOfMeasure.OffOn) return param ? '100' : '0'; if (uom == UnitOfMeasure.Percent) return param ? '100' : '0'; if (uom == UnitOfMeasure.LevelFrom0To255) return param ? '255' : '0'; return param ? '1' : '0'; } } public async sendNodeCommand<P extends string | symbol, N extends ISYNode<any, any, any, any>>(node: N, command: string, defaultParameter?: Utils.MaybeWithUOM, parameters?: Record<P, Utils.MaybeWithUOM<JsonPrimitive> | undefined> | Utils.MaybeWithUOM<JsonPrimitive>): Promise<any> { let uriToUse = `nodes/${node.address}/cmd/${command}?`; if (defaultParameter !== null && defaultParameter !== undefined) { if (Array.isArray(defaultParameter) || isPrimitive(defaultParameter)) { uriToUse += `/${this.#formatDefaultParameter(defaultParameter)}?`; } } if (parameters !== null && parameters !== undefined) { if (typeof parameters === 'object' && !Array.isArray(parameters)) { var q = parameters as Record<P, Utils.MaybeWithUOM<JsonPrimitive> | undefined>; for (const paramName in q) { let s = q[paramName]; if (paramName === 'value' || paramName === 'default') { uriToUse += `/${this.#formatDefaultParameter(s)}?`; continue; } else { let p = this.#formatParameter(paramName, s); if (p === '') continue; uriToUse += `${p}&`; } } //uriToUse += `/${q[((p : Record<string,number|number>) => `${p[]}/${p.paramValue}` ).join('/')}`; } else { uriToUse += `/${this.#formatDefaultParameter(parameters)}`; } } uriToUse = uriToUse.endsWith('?') ? uriToUse.slice(0, -1) : uriToUse.endsWith('&') ? uriToUse.slice(0, -1) : uriToUse; uriToUse = uriToUse.replace('?/', '/'); this.logger.info(`${node.name}: sending ${command} command: ${uriToUse}`); return this.sendRequest(uriToUse, { requestLogLevel: 'info' }); } public async sendRequest<T = any>( url: string, options: { parserOptions?: ParserOptions | X2jOptions; trailingSlash?: boolean; requestLogLevel?: Utils.LogLevel; responseLogLevel?: Utils.LogLevel; errorLogLevel?: Utils.LogLevel; throwOnError?: boolean; } & Utils.ISYRequestConfig = { trailingSlash: true } ): Promise<T> { const requestLogLevel = options.requestLogLevel ?? 'debug'; const responseLogLevel = options.responseLogLevel ?? 'silly'; url = `${url}${options.trailingSlash && !url.endsWith('/') ? '/' : ''}`; const finalUrl = `${this.protocol}://${this.address}/rest/${url}`; const reqOps = { ...this.axiosOptions, ...options }; this.logger.log(requestLogLevel, `Sending request to ${path.join('/rest', url)} with options: ${Utils.logStringify(reqOps, 0)}`); /*{ auth: { username: this.credentials.username, password: this.credentials.password }, baseURL: `${this.protocol}://${this.address}`, }*/ try { delete reqOps.trailingSlash; delete reqOps.requestLogLevel; const response = await axios.get(reqOps.socketPath ? path.join('/rest', url) : finalUrl, reqOps); if (response.data) { if (response.headers['content-type'].toString().includes('xml')) { let altParser = this.xmlParser; if (options.parserOptions) { if (typeof options.parserOptions === 'object') { this.logger.info('Using custom parser options'); altParser = new XMLParser({ ...defaultXMLParserOptions, ...options.parserOptions }); } } // {curParser = new Parser({ ...defaultParserOptions, ...options.parserOptions }); //var altParser = new XMLParser({ ...defaultXMLParserOptions, ...options.parserOptions }); var s = altParser.parse(response.data); this.logger.log(responseLogLevel ?? 'debug', `Response: ${JSON.stringify(s)}`); return s; } else if (response.headers['content-type'].toString().includes('json')) { this.logger.log(responseLogLevel, `Response: ${JSON.stringify(response.data)}`); return JSON.parse(response.data); } else { this.logger.log(responseLogLevel, `Response Header: ${JSON.stringify(response.headers)} Response: ${JSON.stringify(response.data)}`); return response.data; } } } catch (error) { if (options.throwOnError) { throw error; } else { if (options.errorLogLevel) { this.logger.log(options.errorLogLevel, `Error sending request to ISY: ${error?.message}`); } else { this.logger.error(`Error sending request to ISY: ${error?.message}`); } } } } public async sendSetVariable(id: any, type: any, value: any, handleResult: { (success: any): void; (arg0: boolean): void; (arg0: boolean): void }) { const uriToUse = `/rest/vars/set/${type}/${id}/${value}`; this.logger.info(`Sending ISY command...${uriToUse}`); return this.sendRequest(uriToUse); } #variableChangedHandler(variable: { id: string; type: string }) { this.logger.info(`Variable: ${variable.id} (${variable.type}) changed`); } public close() { try { this.webSocket?.close(); } catch (e) { this.logger.error(`Error closing websocket: ${e.message}`); } } // #endregion Public Methods (24) // #region Private Methods (11) #checkForFailure(response: any): boolean { return response === null || response instanceof Error || (response.RestResponse !== undefined && response.RestResponse.status !== 200); } #createVariableKey(type: VariableType, id: number) { return `${type}:${id}`; } #createVariables(type: VariableType, result: any) { for (const variable of result.CList.e) { const id = Number(variable.id); const name = variable.name; const newVariable = new ISYVariable(this, id, name, type); this.variableList.set(this.#createVariableKey(type, id), newVariable); } } async #finishInitialize(success: boolean) { if (!this.nodesLoaded) { this.nodesLoaded = true; //initializeCompleted(); if (success) { // if (this.elkEnabled) { // this.deviceList[this.elkAlarmPanel.address] = this.elkAlarmPanel; // } if (this.enableWebSocket) { await this.initializeWebSocket(); this.guardianTimer = setInterval(this.#guardian.bind(this), this.guardianInterval); } this.emit('initializeCompleted'); } } } async #guardian() { const timeNow = Date.now(); if (timeNow - this.lastActivity > this.guardianInterval) { this.logger.info('Guardian: Detected no activity in more then 60 seconds. Reinitializing web sockets'); await this.refreshStatuses(); await this.initializeWebSocket(); this.guardianTimer.refresh(); this.logger.info('Guardian: WebSocket reinitialized'); this.lastActivity = Date.now(); } } async #readDeviceNodes(obj: { nodes: { node: NodeInfo[] } }) { try { this.logger.info('Reading Device Nodes'); for (const nodeInfo of obj.nodes.node) { try { this.logger.debug(`Loading Device Node: ${JSON.stringify(nodeInfo)}`); if (nodeInfo.enabled) { let newDevice = await DeviceFactory.create(this, nodeInfo); if (!newDevice && nodeInfo.pnode == nodeInfo.address) { this.logger.warn(`Device type resolution failed for ${nodeInfo.name} with type: ${nodeInfo.type} and nodedef: ${nodeInfo.nodeDefId}`); newDevice = new GenericNode(this, nodeInfo); } if (newDevice) { if (this.devices.set) this.devices.set(newDevice.address, newDevice); } /* if (!this.deviceMap.has(nodeInfo.pnode)) { const address = nodeInfo.address; this.deviceMap[nodeInfo.pnode] = { address }; } else { this.deviceMap[nodeInfo.pnode].push(nodeInfo.address); } let newDevice = null; // let deviceTypeInfo = this.isyTypeToTypeName(device.type, device.address); // this.logger.info(JSON.stringify(deviceTypeInfo)); const enabled = nodeInfo.enabled ?? true; const d = await NodeFactory.get(nodeInfo); const m = DeviceFactory.getDeviceDetails(nodeInfo); const cls = m.class ?? d; nodeInfo.property = Array.isArray(nodeInfo.property) ? nodeInfo.property : [nodeInfo.property]; nodeInfo.state = nodeInfo.property.reduce((acc, p) => { if (p && p?.id) { p.name = p.name == '' ? undefined : p.name; acc[p.id] = p; } return acc; }, {}); if (cls) { try { newDevice = new cls(this, nodeInfo); } catch (e) { this.logger.error(`Error creating device ${nodeInfo.name} with type ${nodeInfo.type} and nodedef ${nodeInfo.nodeDefId}: ${e.message}`); continue; } //newDevice = new cls(this, nodeInfo) as ISYDeviceNode<any, any, any, any>; } if (m && newDevice) { newDevice.productName = m.name; newDevice.model = `(${m.modelNumber}) ${m.name} v.${m.version}`; newDevice.modelNumber = m.modelNumber; newDevice.version = m.version; } if (enabled) { if (newDevice === null) { this.logger.warn(`Device type resolution failed for ${nodeInfo.name} with type: ${nodeInfo.type} and nodedef: ${nodeInfo.nodeDefId}`); newDevice = new GenericNode(this, nodeInfo); } else if (newDevice !== null) { if (m.unsupported) { this.logger.warn('Device not currently supported: ' + JSON.stringify(nodeInfo) + ' /n It has been mapped to: ' + d.name); } try { //await newDevice.refreshNotes(); } catch (e) { this.logger.debug('No notes found.'); } // if (!newDevice.hidden) { // } // this.deviceList.push(newDevice); } else { } this.nodeMap.set(newDevice.address, CompositeDevice.isComposite(newDevice) ? newDevice.root : newDevice); if (CompositeDevice.isComposite(newDevice)) { if (this.deviceList.set) this.deviceList.set(newDevice.address, newDevice); } */ } else { this.logger.info(`Ignoring disabled device: ${nodeInfo.name}`); } } catch (e) { this.logger.error(`Error loading device node: ${e.message}`); } } await DeviceFactory.addChildNodes(this); this.logger.info(`${this.nodeMap.size} nodes added.`); for (const node of this.nodeMap.values()) { try { //await node.refreshNotes(); } catch (e) { this.logger.silly('No notes found.'); } } /*for (const node of this.nodeMap.values()) { if (node.parentAddress !== node.address) { let parent = this.deviceList.get(node.parentAddress) as unknown as CompositeDevice<any, any>; if (parent && CompositeDevice.isComposite(parent)) parent.addNode(node); } else if (!this.deviceList.has(node.address)) { this.deviceList.set(node.address, node as unknown as ISYDevice<any, any, any, any>); } }*/ this.logger.info(`${this.devices.size} unique devices added.`); } catch (e) { throw new ISYInitializationError(e, 'readDevices'); } } async #readFolderNodes(result: { nodes: { folder: any } }) { try { this.logger.info('Reading Folder Nodes'); if (result?.nodes?.folder) { for (const folder of result.nodes.folder) { this.logger.debug(`Loading Folder Node: ${JSON.stringify(folder)}`); this.folderMap.set(folder.address, folder.name); } } } catch (e) { throw new ISYInitializationError(e, 'readFolders'); } } async #readSceneNodes(result: { nodes: { group: any } }) { try { this.logger.info('Loading Scene Nodes'); for (const scene of result.nodes?.group) { if (scene.name === 'ISY' || scene.name === 'IoX' || scene.name === 'Auto DR') { continue; } // Skip ISY & Auto DR Scenes const newScene = new ISYScene(this, scene); try { //await newScene.refreshNotes(); } catch (e) { this.logger.debug('No notes found.'); } this.sceneList.set(newScene.address, newScene); } this.logger.info(`${this.sceneList.size} scenes added.`); } catch (e) { throw new ISYInitializationError(e, 'readScenes'); } } #setVariableValues(result: any) { for (const vals of result.vars.var) { const type = Number(vals.type) as VariableType; const id = Number(vals.id); const variable = this.getVariable(type, id); if (variable) { variable.init = vals.init; variable.value = vals.val; variable.lastChanged = new Date(vals.ts); } } } // #endregion Private Methods (11) } export { CompositeDevice } from './Devices/CompositeDevice.js'; export { ISYDeviceNode as DeviceNode } from './Devices/ISYDeviceNode.js'; export { ISYNode as Node } from './ISYNode.js'; export * from './Converters.js'; export * from './Definitions/index.js'; export * as Devices from './Devices/index.js'; export * from './ISYError.js'; export { ISYError } from './ISYError.js'; export { ISYVariable as Variable } from './ISYVariable.js'; export * from './Model/index.js'; export { ISYScene as Scene } from './Scenes/ISYScene.js'; export * from './Utils.js'; export { VariableType } from './VariableType.js'; export namespace ISY { export interface Config extends ISYConfig {} export type Device<F extends Family, D, C, E> = ISYDevice<F, D, C, E>; }