@pmouli/isy-matter-server
Version:
Service to expose an ISY device as a Matter Border router
363 lines • 14.7 kB
JavaScript
import { Driver } from './Definitions/Global/Drivers.js';
import { UnitOfMeasure } from './Definitions/Global/UOM.js';
import { Feature } from './ISY.js';
import { Converter } from './Converters.js';
import { Event } from './Definitions/Global/Events.js';
import { NodeFactory } from './Devices/NodeFactory.js';
import { NodeType } from './NodeType.js';
//type DriverValues<DK extends string | number | symbol,V = any> = {[x in DK]?:V};
export class ISYNode {
// #region Properties (32)
static #displayNameFunction;
#parentNode;
address;
baseLabel;
flag;
isy;
nodeDefId;
static family;
static nodeDefId = 'Unknown';
static implements = [];
commands;
//public readonly formatted: DriverValues<keyof D,string> = {};
//public readonly uom: { [x in Driver.Literal]?: UnitOfMeasure } = { ST: UnitOfMeasure.Boolean };
//public readonly pending: DriverValues<keyof D> = {};
//public readonly local: DriverValues<keyof D> = {};
drivers = {};
enabled;
//TODO: add signature for non-command/non-driver events
events;
//Event.FunctionSigFor<Event.ForAll<E,typeof this>> & Omit<EventEmitter,'on'>
/*{
[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};
};*/
family;
folder = '';
hidden;
isDimmable;
isLoad;
label;
lastChanged;
location;
logger;
// [x: string]: any;
name;
nodeType;
parent;
parentAddress;
parentType;
propsInitialized;
scenes;
spokenName;
type;
features;
// #endregion Properties (32)
// #region Constructors (1)
constructor(isy, node) {
this.isy = isy;
this.isy.nodeMap.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;
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, level = 'debug', ...meta) => {
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, level = 'debug', ...meta) => {
isy.logger[level](`${this.name} (${this.address}): ${msg}`, meta);
return isy.logger;
};
}
this.events = Event.createEmitter(this);
//this.logger(this.nodeDefId);
this.lastChanged = new Date();
}
// #endregion Constructors (1)
// #region Public Getters And Setters (1)
get parentNode() {
if (this.#parentNode === undefined) {
if (this.parentAddress !== this.address && this.parentAddress !== null && this.parentAddress !== undefined) {
this.#parentNode = this.isy.getDevice(this.parentAddress);
if (this.#parentNode !== null) {
//this.#parentNode.addChild(this);
}
}
this.#parentNode = null;
}
return this.#parentNode;
}
// #endregion Public Getters And Setters (1)
// #region Public Methods (18)
addLink(isyScene) {
this.scenes.push(isyScene);
}
_initialized = false;
get initialized() {
if (!this._initialized) {
for (const prop in this.drivers)
if (this.drivers[prop]?.initialized == false && prop != 'ERR')
return false;
}
this._initialized = true;
return true;
}
applyStatus(prop) {
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 {
//@ts-expect-error
this.drivers[prop.id] = Driver.create(prop.id, this, prop, { uom: prop.uom, label: prop.name ?? prop.id, name: prop.name ?? prop.id });
}
}
catch (e) {
this.logger(e?.message ?? e, 'error');
}
}
convert(value, from, to) {
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;
}
}
}
convertFrom(value, uom, propertyName) {
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);
}
}
convertTo(value, uom, propertyName) {
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);
}
}
emit(event, propertyName, newValue, oldValue, formattedValue, controlName) {
//if ('PropertyChanged') return super.emit(event, propertyName, newValue, oldValue, formattedValue);
//else if ('ControlTriggered') return super.emit(event, controlName);
}
generateLabel(template) {
// tslint:disable-next-line: only-arrow-functions
if (!ISYNode.#displayNameFunction) {
// template = template.replace("{", "{this."};
const regex = /(?<op1>\w+) \?\? (?<op2>\w+)/g;
this.logger(`Display name format: ${template}`);
let newttemp = template.replace(regex, "this.$<op1> === null || this.$<op1> === undefined || this.$<op1> === '' ? this.$<op2> : this.$<op1>");
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);
}
async getNotes() {
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;
}
}
handleControlTrigger(controlName) {
//this.lastChanged = new Date();
//this.events.emit(`${this.commands[controlName].name}`, controlName);
return true;
}
handleEvent(event) {
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, 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;
}
}
handlePropertyChange(propertyName, value, uom, prec, formattedValue) {
this.lastChanged = new Date();
let driver = this.drivers[propertyName];
/*this.logger(`Driver ${propertyName} (${driver?.label} value update ${value} (${formattedValue}) uom: ${UnitOfMeasure[uom]} event received.`);*/
const oldValue = driver?.state.value;
const oldValueRaw = driver?.state.rawValue;
if (driver?.patch(value, formattedValue, uom, prec)) {
this.logger(`Driver ${driver.label} updated from ${oldValue} (${oldValueRaw}) to ${driver.state.value} (${driver.state.rawValue})`);
//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;
}*/
parseResult(node) {
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;
}
}
async readProperties() {
var result = await this.isy.sendRequest(`nodes/${this.address}/status`);
this.logger(JSON.stringify(result), 'debug');
return result.property;
}
/*public addChild<K extends ISYDeviceNode<T, any, any, any>>(childDevice: K) {
this.children.push(childDevice);
}*/
async readProperty(propertyName) {
var result = await this.isy.sendRequest(`nodes/${this.address}/${propertyName}`);
return result.property;
}
async refreshState() {
const device = this;
const node = (await this.isy.sendRequest(`nodes/${this.address}/status`)).node;
// this.logger(node);
this.parseResult(node);
return node;
}
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);
}
}
async sendCommand(command, valueOrParameters, parameters) {
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 });
}
async updateProperty(propertyName, value) {
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;
});
}
}
(function (ISYNode) {
function getImplements(node) {
return NodeFactory.getImplements(node);
}
ISYNode.getImplements = getImplements;
})(ISYNode || (ISYNode = {}));
//# sourceMappingURL=ISYNode.js.map