@pmouli/isy-matter-server
Version:
Service to expose an ISY device as a Matter Border router
327 lines (266 loc) • 13.1 kB
text/typescript
import { ClusterBehavior } from '@matter/main';
import { ISYDeviceNode } from 'isy-nodejs/Devices/ISYDeviceNode';
import { type DeviceNode, type Driver, type Factory, CompositeDevice, Converter } from 'isy-nodejs/ISY';
import { ISYDevice } from 'isy-nodejs/ISYDevice';
import type { DriversOf } from 'isy-nodejs/ISYNode';
import type { Constructor } from 'type-fest';
import winston from 'winston';
import { type ClusterMapping, hasPath } from '../Mappings/MappingRegistry.js';
import { ISYBridgedDeviceBehavior } from './ISYBridgedDeviceBehavior.js';
// #region Type aliases (6)
export type ClusterForBehavior<B> = B extends ClusterBehavior.Type<infer C, infer D, infer E> ? C : never;
export interface DeviceBehavior<P extends CompositeDevice.Any | DeviceNode.Any, T extends { cluster? }> {
device: P;
bridgedDeviceBehavior: ISYBridgedDeviceBehavior<P>;
logger: winston.Logger;
//ts-ignore
map: ClusterMapping<T['cluster'], P>;
handlePropertyChange(chg: PropertyChange<P>): void;
}
type NotUnknown<T extends ClusterBehavior> = T extends { cluster: { name: 'Unknown' } } ? never : T;
export type PropertyChange<P extends CompositeDevice.Any | DeviceNode.Any> =
P extends DeviceNode.Any ?
{
driver: keyof P['drivers'];
newValue: any;
oldValue: any;
formattedValue: string;
}
: P extends CompositeDevice.Any ? { node: keyof P['nodes']; driver: keyof P['nodes'][keyof P['nodes']]['drivers']; newValue: any; oldValue: any; formattedValue: string }
: never;
// #endregion Type aliases (6)
// #region Functions (1)
function clusterBehaviorForNode<T extends Constructor<ClusterBehavior> & { cluster }, P extends DeviceNode.Any>(base: T, p: Factory<P>): typeof base & { new (...args: any[]): DeviceBehavior<P, T> } & { factory: typeof p } {
//@ts-ignore
let s = class ISYClusterBehavior extends base implements DeviceBehavior<P, T> {
_device: P;
static factory = p;
handlers: { [x in DriversOf<P>]: (newValue, oldValue, formattedValue) => void } = {} as any;
_bridgedDeviceBehavior: ISYBridgedDeviceBehavior<P>;
get bridgedDeviceBehavior(): ISYBridgedDeviceBehavior<P> {
return this._bridgedDeviceBehavior ?? this.agent.get(ISYBridgedDeviceBehavior<P>);
}
///public map: ClusterMapping<ToClusterTypeByName<ClusterForBehavior<ConstructedType<typeof base>>["name"]>,ISYDeviceNode<any, any, any>>;
map: ClusterMapping<T['cluster'], P>;
setStateSafe(key: string, value: any) {
if (this.state[key] != value) {
try {
this.state[key] = value;
} catch (e) {
this.logger.error(`Error setting state ${key} to ${value}: ${e}`);
}
}
}
override async initialize(_options?: {}) {
await super.initialize(_options);
var behavior = (await this.agent.load(ISYBridgedDeviceBehavior<P>)) as ISYBridgedDeviceBehavior<P>;
this._bridgedDeviceBehavior = behavior;
//var behavior = this.agent.get(ISYBridgedDeviceBehavior);
this._device = behavior.device as P & ISYDeviceNode.Any;
this.logger.debug(`Initializing cluster behavior: ${this.constructor.name}`);
//@ts-ignore
this.map = behavior.mapForBehavior<T, P>(this);
for (const key2 in this.map?.attributes) {
let val = this.map.attributes[key2];
let that = this;
let driverObj = null as Driver<any, any>;
if (typeof val === 'string' || typeof val === 'number') {
driverObj = this._device.drivers[val] as Driver<any, any>;
if (driverObj) {
let evt = `${driverObj.name}Changed`;
//this.state[key2 as string] = driverObj.value;
//this.state[key2 as string] = driverObj.value;
this.setStateSafe(key2, driverObj.value);
this[`handle_${evt}`] = function ({ driver, newValue, oldValue, formattedValue }) {
this.logger.debug(`${that.constructor.name}: handling property change for ${String(driver)} from ${oldValue} to ${newValue}`);
this.setStateSafe(key2, newValue);
};
this.reactTo(behavior.events[evt], this[`handle_${evt}`]);
}
} else if (hasPath(val)) {
driverObj = this._device.drivers[val.driver as string] as Driver<any, any>;
if (driverObj) {
let evt = `${driverObj.name}Changed`;
let { driver, converter } = val;
const convFunc = Converter.get(converter as any)?.to;
if (!convFunc) throw new Error(`Converter ${converter} not found`);
/*this.state[key2 as string] = convFunc(this._device.drivers[driver as string].value);*/
this.setStateSafe(key2, convFunc(this._device.drivers[driver as string].value));
this[`handle_${evt}`] = function ({ driver, newValue, oldValue, formattedValue }) {
let v = convFunc(newValue);
let oldV = this.state[key2 as string];
this.logger.debug(`${that.constructor.name}: handling property change for ${String(driver)} from ${oldValue} (${oldV}) to ${newValue} (${v})`);
this.setStateSafe(key2, v);
};
this.reactTo(behavior.events[evt], this[`handle_${evt}`]);
}
}
/*this.handlers[driverObj.id] = function (parent = that, newValue, oldValue, formattedValue) {
//this.device.logger(`Handling property change for ${driver} (${key2}) with value ${newValue}`);
//if (convFunc) this.state[key2 as string] = convFunc(newValue);
parent.state[key2 as string] = convFunc(newValue);
};*/
//(this as any).evt = this.handlers[driverObj.name];
}
this.logger.debug(`Cluster behavior initialized: ${this.constructor.name}`);
//this.reactTo(behavior.events.propertyChanged, this.handlePropertyChange, { lock: false });
//this._device.on("PropertyChanged", this.handlePropertyChange.bind(this));
}
get device(): P & ISYDeviceNode.Any {
return (this._device = this._device ?? (this.agent.get(ISYBridgedDeviceBehavior<P>).device as P & ISYDeviceNode.Any)) as P & ISYDeviceNode.Any;
}
get logger() {
return this.bridgedDeviceBehavior.logger;
}
async handlePropertyChange({ driver, newValue, oldValue, formattedValue }: PropertyChange<P>) {
// for (const key2 in this.map.attributes) {
//await this.initialize();
this.device.logger(`${this.constructor.name}: handling property change for ${String(driver)} with value ${newValue}`);
if (this.handlers[driver]) {
this.handlers[driver](newValue, oldValue, formattedValue);
}
// if (typeof this.map.attributes[key2] === "string") {
// if(this.map.attributes[key2] == driver)
// {
// this.state[key2 as string] = newValue;
// return;
// } }else if (this.map.attributes[key2].driver == driver) {
// if (this.map.attributes[key2]?.driver == driver) {
// this.state[key2 as string] = this.map.attributes[key2].converter(newValue);
// }
// }
// }
}
};
s.factory = p;
return s;
}
function clusterBehaviorForComposite<T extends Constructor<ClusterBehavior> & { cluster }, P extends CompositeDevice.Any>(base: T, p: Factory<P>): typeof base & { new (...args: any[]): DeviceBehavior<P, T> } & { factory: typeof p } {
let s = class ISYClusterBehavior extends base implements DeviceBehavior<P, T> {
_device: P;
static factory = p;
handlers: {
[x in keyof P['drivers']]: {
[y in keyof P['drivers'][x]]: (newValue, oldValue, formattedValue) => void;
};
} = {} as any;
setStateSafe(key: string, value: any) {
if (this.state[key] != value) {
try {
this.state[key] = value;
} catch (e) {
this.logger.error(`Error setting state ${key} to ${value}: ${e}`);
}
}
}
_bridgedDeviceBehavior: ISYBridgedDeviceBehavior<P>;
get bridgedDeviceBehavior(): ISYBridgedDeviceBehavior<P> {
return this._bridgedDeviceBehavior ?? this.agent.get(ISYBridgedDeviceBehavior<P>);
}
///public map: ClusterMapping<ToClusterTypeByName<ClusterForBehavior<ConstructedType<typeof base>>["name"]>,ISYDeviceNode<any, any, any>>;
map: ClusterMapping<T['cluster'], P>;
override async initialize(_options?: {}) {
await super.initialize(_options);
var behavior = (await this.agent.load(ISYBridgedDeviceBehavior<P>)) as ISYBridgedDeviceBehavior<P>;
this._bridgedDeviceBehavior = behavior;
//var behavior = this.agent.get(ISYBridgedDeviceBehavior);
this._device = behavior.device as P & CompositeDevice.Any;
this.logger.debug(`Initializing cluster behavior: ${this.constructor.name}`);
//@ts-ignore
this.map = behavior.mapForBehavior<T, P>(this);
for (const key in this.map?.attributes) {
let val = this.map.attributes[key];
let driverObj = null as Driver<any, any>;
let nodeObj = null as DeviceNode.Any;
if (typeof val === 'string') {
let [node, id] = val.split('.');
nodeObj = this._device.nodes[node as string] as DeviceNode.Any;
driverObj = nodeObj.drivers[id as string] as Driver<any, any>;
this.setStateSafe(key, nodeObj.drivers[id].value);
//this.state[key as string] = nodeObj.drivers[id].value;
let evt = `${node}.${driverObj.name}Changed`;
this[`handle_${evt}`] = function ({ node, driver, newValue, oldValue, formattedValue }) {
this.logger.debug(`${this.constructor.name}: handling property change for ${node}.${driver} (${key}) with value ${newValue}`);
this.setStateSafe(key, newValue);
};
this.reactTo(behavior.events[evt], this[`handle_${evt}`]);
} else if (hasPath(val)) {
let driverObj = this._device?.nodes[val.node]?.drivers[val.driver] as Driver<any, any>;
if (driverObj) {
let evt = `${val.node}.${driverObj.name}Changed`;
if (val.converter) {
const convFunc = Converter.get(val.converter as any)?.to;
if (!convFunc) throw new Error(`Converter ${val.converter} not found`);
this.setStateSafe(key, convFunc(driverObj.value));
//this.state[key as string] = convFunc(driverObj.value);
this[`handle_${evt}`] = function ({ node, driver, newValue, oldValue, formattedValue }) {
let v = convFunc(newValue);
let oldV = this.state[key as string];
this.logger.debug(`${this.constructor.name}: handling property change for ${node}.${String(driver)} from ${oldValue} (${oldV}) to ${newValue} (${v})`);
this.setStateSafe(key, v);
};
} else {
this.setStateSafe(key, driverObj.value);
//this.state[key as string] = driverObj.value;
this[`handle_${evt}`] = function ({ node, driver, newValue, oldValue, formattedValue }) {
this.logger.debug(`${this.constructor.name}: handling property change for ${node}.${driver} (${key}) from ${oldValue} to ${newValue}`);
this.setStateSafe(key, newValue);
};
}
this.reactTo(behavior.events[evt], this[`handle_${evt}`]);
}
}
/*if (driverObj && nodeObj) {
let evt = `${nodeObj}.${driverObj.name}Changed`;
(this as any).evt = this.handlers[driverObj.name];
//this.reactTo(behavior.events[evt], this.handlePropertyChange, { lock: false });
}*/
this.logger.debug(`Cluster behavior initialized: ${this.constructor.name}`);
}
//this.reactTo(behavior.events.propertyChanged, this.handlePropertyChange, { lock: false });
//this._device.on("PropertyChanged", this.handlePropertyChange.bind(this));
}
get device(): P & ISYDeviceNode.Any {
return (this._device = this._device ?? (this.agent.get(ISYBridgedDeviceBehavior<P>).device as P & ISYDeviceNode.Any)) as P & ISYDeviceNode.Any;
}
get logger() {
return this.bridgedDeviceBehavior.logger;
}
async handlePropertyChange({ node, driver, newValue, oldValue, formattedValue }: PropertyChange<P>) {
// for (const key2 in this.map.attributes) {
//await this.initialize();
this.logger.debug(`${this.constructor.name}: handling property change for ${node}.${String(driver)} with value ${newValue}`);
if (this.handlers[node][driver as string]) {
this.handlers[node][driver as string](newValue, oldValue, formattedValue);
}
// if (typeof this.map.attributes[key2] === "string") {
// if(this.map.attributes[key2] == driver)
// {
// this.state[key2 as string] = newValue;
// return;
// } }else if (this.map.attributes[key2].driver == driver) {
// if (this.map.attributes[key2]?.driver == driver) {
// this.state[key2 as string] = this.map.attributes[key2].converter(newValue);
// }
// }
// }
}
};
s.factory = p;
return s;
}
export function ISYClusterBehavior<T extends Constructor<ClusterBehavior> & { cluster } & Pick<ClusterBehavior.Type, 'alter'>, P extends CompositeDevice.Any | DeviceNode.Any>(base: T, p: Factory<P>): typeof base & { new (...args: any[]): DeviceBehavior<P, T> } & { factory: Factory<P> } {
let alterations = { attributes: {} };
for (const l in base.cluster.attributes) {
alterations.attributes[l] = { persistent: false };
}
base = (base as unknown as ClusterBehavior.Type).alter(alterations) as unknown as T;
if (ISYDevice.isNode(p)) {
//@ts-ignore
return clusterBehaviorForNode(base, p);
} else if (ISYDevice.isComposite(p)) {
//@ts-ignore
return clusterBehaviorForComposite(base, p);
}
}
// #endregion Functions (1)