@pmouli/isy-matter-server
Version:
Service to expose an ISY device as a Matter Border router
907 lines (899 loc) • 38.2 kB
JavaScript
import axios from 'axios';
import { EventEmitter } from 'events';
import { format, Logger, loggers } from 'winston';
import WebSocket from 'ws';
import { 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 { EventType } from './Events/EventType.js';
import { ISYVariable } from './ISYVariable.js';
import { ISYScene } from './Scenes/ISYScene.js';
import { VariableType } from './VariableType.js';
import * as Utils from './Utils.js';
import { XMLParser } from 'fast-xml-parser';
import path from 'path';
import { UnitOfMeasure } from './Definitions/index.js';
import { GenericNode } from './Devices/GenericNode.js';
import { ISYError } from './ISYError.js';
import { isPrimitive, writeDebugFile } from './Utils.js';
class ISYInitializationError extends ISYError {
step;
constructor(messageOrError, step) {
super(messageOrError);
this.name = 'ISYInitializationError';
this.step = step;
}
}
const defaultParserOptions = {
explicitArray: false,
mergeAttrs: true,
attrValueProcessors: [parseNumbers, parseBooleans],
valueProcessors: [parseNumbers, parseBooleans],
tagNameProcessors: [(tagName) => (tagName === 'st' || tagName === 'cmd' || tagName === 'nodeDef' ? '' : tagName)]
};
const defaultXMLParserOptions = {
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 = {};
export class ISY extends EventEmitter {
// #region Properties (30)
credentials;
devices = new Map();
deviceMap = new Map();
displayNameFormat;
elkEnabled;
enableWebSocket;
folderMap = new Map();
host;
nodeMap = new Map();
port;
protocol;
sceneList = new Map();
storagePath;
variableList = new Map();
wsprotocol = 'ws';
zoneMap = new Map();
static instance;
configInfo;
elkAlarmPanel;
guardianTimer;
id;
lastActivity;
logger;
model;
nodesLoaded = false;
productId = 5226;
productName = 'eisy';
firmwareVersion;
vendorName = 'Universal Devices, Inc.';
webSocket;
apiVersion;
socketPath;
xmlParser;
axiosOptions;
guardianInterval;
static async create(config, logger, storagePath) {
if (!ISY.instance) {
ISY.instance = new ISY(config, logger, storagePath);
await ISY.instance.initialize();
}
return ISY.instance;
}
// #endregion Properties (30)
// #region Constructors (1)
constructor(config, logger = new Logger(), storagePath) {
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]() {
if (ISY.instance === this) {
ISY.instance = null;
}
this.close();
this.logger.info('ISY instance disposed.');
}
[Symbol.asyncDispose]() {
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)
get address() {
return `${this.host}:${this.port}`;
}
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);
}*/
getDevice(address, parentsOnly = false) {
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;
}
getElkAlarmPanel() {
return this.elkAlarmPanel;
}
getNode(address, parentsOnly = false) {
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;
}
getScene(address) {
return this.sceneList.get(address);
}
getVariable(type, id) {
const key = this.#createVariableKey(type, id);
if (this.variableList.has(key)) {
return this.variableList[key];
}
return null;
}
getVariableList() {
return this.variableList;
}
async handleInitializeError(step, reason) {
this.logger.error(`Error initializing ISY (${step}): ${Utils.logStringify(reason)}`);
return Promise.reject(reason);
}
handleWebSocketMessage(event) {
this.lastActivity = Date.now();
let that = this;
parser.parseString(event.data, (err, res) => {
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?.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)}`);
}
});
}
async initialize() {
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;
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((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, response) => {
that.logger.warn(`Websocket subscription error: ${err}`);
reject(new ISYInitializationError('Websocket subscription error', 'websocket'));
})
.on('fail', (data, response) => {
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) => {
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, reason) => {
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');
}
}
async loadConfig() {
try {
this.logger.info('Loading ISY Config');
const configuration = (await this.sendRequest('config')).configuration;
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');
}
}
async loadNodes() {
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');
}
}
}
async loadVariables(type) {
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]);
}
}
nodeChangedHandler(node, 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)
}*/
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);
if (device !== null && device !== undefined) {
device.parseResult(node);
}
}
}
catch (e) {
throw new Error(`Error refreshing statuses: ${JSON.stringify(e.message)}`);
}
}
async sendGetVariable(id, type, handleResult) {
const uriToUse = `vars/get/${type}/${id}`;
return this.sendRequest(uriToUse).then((p) => handleResult(p.val, p.init));
}
async sendISYCommand(path) {
// const uriToUse = `${this.protocol}://${this.address}/rest/${path}`;
this.logger.info(`Sending command...${path}`);
return this.sendRequest(path);
}
#formatParameter(name, value) {
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) {
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, uom) {
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';
}
}
async sendNodeCommand(node, command, defaultParameter, parameters) {
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;
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' });
}
async sendRequest(url, options = { trailingSlash: true }) {
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}`);
}
}
}
}
async sendSetVariable(id, type, value, handleResult) {
const uriToUse = `/rest/vars/set/${type}/${id}/${value}`;
this.logger.info(`Sending ISY command...${uriToUse}`);
return this.sendRequest(uriToUse);
}
#variableChangedHandler(variable) {
this.logger.info(`Variable: ${variable.id} (${variable.type}) changed`);
}
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) {
return response === null || response instanceof Error || (response.RestResponse !== undefined && response.RestResponse.status !== 200);
}
#createVariableKey(type, id) {
return `${type}:${id}`;
}
#createVariables(type, result) {
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) {
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) {
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) {
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) {
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) {
for (const vals of result.vars.var) {
const type = Number(vals.type);
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);
}
}
}
}
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';
//# sourceMappingURL=ISY.js.map