UNPKG

@pmouli/isy-matter-server

Version:

Service to expose an ISY device as a Matter Border router

886 lines (777 loc) 24.6 kB
import { camelize, capitalize } from '@matter/general'; import camelcase from 'camelcase'; import { getRandomValues } from 'crypto'; //import { detailedDiff } from 'deep-object-diff'; import { create } from 'domain'; import * as fs from 'fs'; import diff from 'microdiff'; import { camelCase, merge, pascalCase } from 'moderndash'; import { ThisTypeNode, type TryStatement } from 'ts-morph'; import ts, { ClassElement, factory, SyntaxKind } from 'typescript'; import {} from 'util'; import type { Driver } from '../Definitions/Global/Drivers.js'; import { Family } from '../Definitions/Global/Families.js'; import { UnitOfMeasure } from '../Definitions/Global/UOM.js'; import { toArray } from '../Utils.js'; import { EditorDef, type EditorDefMap as EDM, type RangeDef } from './EditorDef.js'; import { EnumDefinition, EnumDefinitionMap } from './EnumDefinition.js'; import { IndexDef, NLS, NLSCommandParameterRecord, NLSCommandRecord, NLSDriverRecord, NLSGenericRecord, NLSRecord, NLSRecordType, type NLSIndexMap as NLSI, type NLSRecordMap as NLSM, type NLSRecordTypeMap } from './NLS.js'; import { DriverDef, NodeDef, SendCommandDef, type AcceptCommandDef, type ParamDef } from './NodeDef.js'; function buildNodeClassDefinitions<T extends Family>(nodeDefs: NodeDef[], family: T): { [x: string]: NodeClassDefinition<T> } { const map: { [x: string]: NodeClassDefinition<T> } = {}; for (const nodeDef of nodeDefs) { var f = new NodeClassDefinition(nodeDef, family); f.applyNLS(); f.applyEditorDefs(); f.applyIndexDefs(); for (const driver of Object.values(f.drivers)) { for (const cmd of Object.values(f.commands)) { if (cmd.initialValue == driver.id) { driver.readonly = false; driver.settable = true; break; } if (cmd.parameters) for (const cmdp of cmd.parameters) { if (cmdp.initialValue === driver.id) { driver.readonly = false; driver.settable = true; break; } } } } map[f.id] = f; } for (const id in map) { let node = map[id]; for (const id2 of Object.keys(map).filter((k) => id !== k)) { if (map[id2].implements.includes(id) || node.implements.includes(id2) || node.equivalentTo.includes(id2)) { } else { const node2 = map[id2]; const d = diff(JSON.parse(JSON.stringify(node2)), JSON.parse(JSON.stringify(node))); let ext = false; let equiv = false; if (d.filter((p) => p.type == 'REMOVE').length == 0) { ext = true; if (d.filter((p) => p.type == 'CREATE').length == 0) equiv = true; } if (ext && equiv) { if (id.includes(id2)) { node.implements.push(id2, ...node2.implements); node.equivalentTo.push(id2); node2.equivalents.push(id); } else { node2.implements.push(id, ...node.implements); node2.equivalentTo.push(id); node.equivalents.push(id2); } } else if (ext) { node.implements.push(id2, ...node2.implements); if (id.includes(id2)) { node.extends.push(id2); } } // if (Object.keys(d.).length == 0) { // node.extends.push(id2); // if (Object.keys(d.added).length == 0) { // node.equivalentTo.push(id2); // } // } //console.log(d); } } map[id] = node; } // for (const node of Object.values(map)) // { // let s = Array.from(node.implements); // for (const i of s) // { // if (map[i].equivalentTo.length > 0) // { // node.implements(i);] // } // } // } //TODO: Make this recursive for (const node of Object.values(map).filter((p) => p.equivalentTo.length == 0)) { if (node.extends == undefined && node.implements.length > 0) { let e: string = null; for (const i of node.implements.filter((p) => map[p].equivalentTo.length == 0)) { let superNode = map[i]; if (e == null || superNode.implements.includes(e)) { e = i; } } } map[node.id] = node; } for (const node of Object.values(map).filter((p) => p.implements.length > 0)) { for (const i of node.implements) { let superNode = map[i]; if (superNode) superNode.implementedBy?.push(node.id); } } return map; } export type NCD = NodeClassDefinition<Family>; export class NodeClassDefinition<T extends Family> { // #region Properties (11) public commands: { [x: string]: CommandDefinition } = {}; public drivers: { [x in Driver.Type]?: DriverDefinition } = {}; public dynamic: boolean = false; public equivalentTo: string[] = []; public equivalents: string[] = []; public events: { [x: string]: EventDefinition } = {}; public extends?: string[] = []; public family: T; public id: string; public implements: string[] = []; public implementedBy: string[] = []; public label: string; public nlsId: string; // #endregion Properties (11) // #region Constructors (1) constructor(nodeDef: NodeDef, family: T) { this.id = nodeDef.id; this.nlsId = nodeDef.nls; this.family = family; for (const st of toArray(nodeDef.sts?.st)) { this.drivers[st.id as Driver.Type] = new DriverDefinition(st, this); // Object.defineProperty(this.drivers, st.id, { // value: new DriverDefinition(st), // enumerable: true, // writable: true, // configurable: true}); } for (const cmd of toArray(nodeDef.cmds?.accepts?.cmd)) { this.commands[cmd.id] = new CommandDefinition(cmd, this); // Object.defineProperty(this.drivers, st.id, { // value: new DriverDefinition(st), // enumerable: true, // writable: true, // configurable: true}); } for (const cmd of toArray(nodeDef.cmds?.sends?.cmd)) { this.events[cmd.id] = new EventDefinition(cmd, this); // Object.defineProperty(this.drivers, st.id, { // value: new DriverDefinition(st), // enumerable: true, // writable: true, // configurable: true}); } } // #endregion Constructors (1) // #region Public Getters And Setters (1) public get name(): string { return `${pascalCase(this.label ?? this.id ?? 'Unknown')}`; } // #endregion Public Getters And Setters (1) // #region Public Methods (6) public applyEditorDefs() { for (const driver of Object.values(this.drivers)) { if (driver.editorId) { let d = EditorDef.get(this.family, driver.editorId); if (d) driver.applyEditorDef(d); } } for (const cmd of Object.values(this.commands)) { if (cmd.editorId) { let d = EditorDef.get(this.family, cmd.editorId); if (d) cmd.applyEditorDef(d); } if (cmd.parameters) { for (const p of cmd.parameters) { if (p.editorId) { let d = EditorDef.get(this.family, p.editorId); if (d) p.applyEditorDef(d); } } } } } public applyIndexDefs() { let NLSIndexMap = IndexDef.Map; if (NLSIndexMap.has(Family.Generic)) { this.applyIndexMap(NLSIndexMap.get(Family.Generic)); } if (NLSIndexMap.has(this.family)) { this.applyIndexMap(NLSIndexMap.get(this.family)); } } public applyIndexMap(indexDef: { [x: string]: { [y: number]: string } }) { for (const driver in this.drivers) { this.drivers[driver as Driver.Type].applyIndexDef(indexDef); } for (const cmd in this.commands) { this.commands[cmd].applyIndexDef(indexDef); } for (const cmd in this.events) { this.events[cmd].applyIndexDef(indexDef); } } public applyNLS() { let NLSRecordMap = NLS.Map; if (NLSRecordMap.has(Family.Generic)) { this.applyNLSMap(NLSRecordMap.get(Family.Generic)); } if (NLSRecordMap.has(this.family)) { this.applyNLSMap(NLSRecordMap.get(this.family)); } } public applyNLSMap(nlsm: { [y: string]: NLSRecordTypeMap }) { let nls = null; if (nlsm['Generic']) { nls = nlsm['Generic']; this.applyNLSRecords(nls); } if (this.nlsId && nlsm[this.nlsId]) { nls = nlsm[this.nlsId]; this.applyNLSRecords(nls); } } public toJSON() { return { className: this.name ?? this.label, id: this.id, nlsId: this.nlsId, name: this.name, label: this.label ?? this.id, drivers: this.drivers, commands: this.commands, events: this.events, family: this.family, dynamic: [Family.ZWave, Family.ZigBee, Family.ZMatter].includes(this.family), implements: this.implements.length > 0 ? this.implements : undefined, equivalentTo: this.equivalentTo.length > 0 ? this.equivalentTo : undefined, extends: this.extends.length > 0 ? this.extends : undefined, equivalents: this.equivalents.length > 0 ? this.equivalents : undefined, implementedBy: this.implementedBy.length > 0 ? this.implementedBy : undefined }; } // #endregion Public Methods (6) // #region Private Methods (1) private applyNLSRecords(nls: NLSRecordTypeMap) { for (const entry of nls.GEN ?? []) { //if (this.commands.hasOwnProperty(entry.key)) { for (const cmd in this.commands) { this.commands[cmd].applyNLSRecord(entry); } //} if (this.events.hasOwnProperty(entry.control)) { this.events[entry.control].applyNLSRecord(entry); } if (this.drivers.hasOwnProperty(entry.control)) { this.drivers[entry.control].applyNLSRecord(entry); } } for (const entry of nls.ST ?? []) { var e = entry as NLSDriverRecord; if (this.drivers.hasOwnProperty(e.control)) { this.drivers[e.control].applyNLSRecord(e); } } for (const entry of nls.CMD ?? []) { var c = entry as NLSCommandRecord; if (this.commands.hasOwnProperty(c.control)) { this.commands[c.control].applyNLSRecord(c); } if (this.events.hasOwnProperty(c.control)) { this.events[c.control].applyNLSRecord(c); } } for (const entry of nls.CMDP ?? []) { var cp = entry as NLSCommandParameterRecord; for (const cmd in this.commands) { this.commands[cmd].applyNLSRecord(cp); } } for (const entry of nls.CMDPN ?? []) { var cp = entry as NLSCommandParameterRecord; for (const cmd in this.commands) { this.commands[cmd].applyNLSRecord(cp); } } if (Array.isArray(nls.ND)) { for (const entry of nls.ND) { if (entry.nodeDefId == this.id && entry.property == 'NAME') { this.label = entry.value; } } } else if (nls.ND) { let ND = nls.ND as { nodeDefId: string; property: string; value: string }; if (ND.nodeDefId == this.id && ND.property == 'NAME') { this.label = ND.value; } } if (Array.isArray(nls.NDN)) { for (const entry of nls.NDN) { if (entry.nlsId == this.nlsId && entry.property == 'NAME') { this.label = entry.value; } } } else if (nls.NDN) { let ND = nls.NDN as { nlsId: string; property: string; value: string }; if (ND.nlsId == this.nlsId && ND.property == 'NAME') { this.label = ND.value; } } } // #endregion Private Methods (1) // toTypeScript(): ClassDeclaration { // ts.createClassDeclaration( // undefined, // [ts.createModifier(ts.SyntaxKind.PublicKeyword)], // ts.createIdentifier(this.name), // undefined // } } export type DataTypeDefinition = | { uom: UnitOfMeasure; serverUom?: UnitOfMeasure; enum: false; indexId?: string; min: number; max: number; step?: number; precision?: number; returnType?: string; } | { uom: UnitOfMeasure; enum: true; indexId: string; values: { [x: number]: string; }; returnType?: string; }; export abstract class NodeMemberDefinition<TId extends string> { // #region Properties (7) public classDef: NodeClassDefinition<any>; public dataType: DataTypeDefinition[]; public editorId: string; public hidden: boolean; public id: TId; public label?: string; public optional: boolean; // #endregion Properties (7) // #region Constructors (1) constructor(classDef: NodeClassDefinition<any>, def?: DriverDef | ParamDef) { this.classDef = classDef; if (def) { this.editorId = def.editor; this.dataType = []; var r = this.parseEditorId(def.editor); if (r) this.applyEditorDef({ id: def.editor, range: r }); } } // #endregion Constructors (1) // #region Public Getters And Setters (1) public get name(): string { return camelCase(this.label ?? this.id); } // #endregion Public Getters And Setters (1) // #region Public Methods (6) public applyEditorDef(e: EditorDef) { if (e.id === this.editorId) { var d = []; for (const rangeDef of toArray(e.range)) { let dt = {} as DataTypeDefinition; if ('subset' in rangeDef) { dt = { uom: rangeDef.uom, indexId: rangeDef.nls, ...this.#parseSubset(e) }; } else { dt = { uom: rangeDef.uom, enum: false, min: rangeDef.min, max: rangeDef.max, step: rangeDef.step ?? undefined, precision: rangeDef.prec ?? undefined, indexId: rangeDef.nls ?? undefined, returnType: rangeDef.nls ? EnumDefinition.get(this.classDef.family, rangeDef.nls, rangeDef.uom, this.classDef.id)?.name : undefined }; } if (rangeDef.uom === UnitOfMeasure.Index) { dt.enum = true; } d.push(dt); } this.dataType = d; } } public applyIndexDef(e: { [x: string]: { [y: number]: string } }) { if (this.dataType) { for (const uom in this.dataType) { var s = this.dataType[uom]; if (s.enum && e[s.indexId]) { for (const v in s.values) { s.values[v] = e[s.indexId][v]; } } } } } public parseEditorId(e: string) { if (e.startsWith('_')) { var rangeDef = {} as RangeDef; let field = 'uom'; const tokens = e.split('_').slice(1); for (let token of tokens) { const curField = field; if (token == 'R') { field = 'min'; continue; } else if (token == 'S') { field = 'lowMask'; continue; } else if (token == 'N') { field = 'nls'; continue; } if (curField == 'uom') { field = 'prec'; } else if (curField == 'prec' || curField == 'max' || curField == 'highMask') { field = null; } else if (curField == 'min') { field = 'max'; } else if (curField == 'lowMask') { field = 'highMask'; } if (curField == 'lowMask') { rangeDef['subset'] = token; } else if (curField == 'highMask') { if (rangeDef['subset'] !== undefined) { rangeDef['subset'] += `,${token}`; } else { rangeDef['subset'] = token; } } else if (curField == 'nls') { if (rangeDef[curField]) rangeDef[curField] += '_' + token; else rangeDef[curField] = token; } else if (curField !== null && curField !== undefined) { rangeDef[curField] = Number(token.replace('m', '-')); } } return rangeDef; } } public primaryDataType(): { uom: number; enum: false; min: number; max: number; step?: number; precision?: number } | { uom: number; enum: true; indexId: string; values: { [x: number]: string } } { return Object.values(this.dataType)[0]; } public selectValue(currentValue, newValue, nlsId) { if (currentValue === undefined || currentValue === null) { return newValue; } else if (this.classDef.nlsId == nlsId) { return newValue; } return currentValue; } public toJSON() { return { id: this.id, name: this.name, label: this.label, hidden: this.hidden, optional: this.optional, editorId: this.editorId, dataType: this.dataType }; } // #endregion Public Methods (6) // #region Private Methods (1) #parseSubset(e: EditorDef): { enum: true; values: { [x: number]: string } } | { enum: false; min: number; max: number } { if ('subset' in e.range) { if (e.range.subset.includes(',')) { var values = {}; for (const v of e.range.subset.split(',')) { let val = Number(v); if (Number.isNaN(val)) { let [k, q] = v.split('-'); for (let i = Number(k); i <= Number(q); i++) { values[i] = ''; } } else { values[Number(v)] = ''; } } return { enum: true, values: values }; } if (e.range.subset.includes('-')) { const r = e.range.subset.split('-'); return { enum: false, min: Number(r[0]), max: Number(r[1]) }; } } } // #endregion Private Methods (1) } export class DriverDefinition extends NodeMemberDefinition<Driver.Type> { // #region Properties (1) public readonly = true; public settable = true; // #endregion Properties (1) // #region Constructors (1) constructor(def: DriverDef, classDef: NodeClassDefinition<any>) { super(classDef, def); this.id = def.id; this.hidden = def.hide === 'T'; this.editorId = def.editor; this.optional = false; this.settable = false; } // #endregion Constructors (1) // #region Public Methods (2) public applyNLSRecord(nls: NLSGenericRecord | NLSDriverRecord) { if (nls.type === NLSRecordType.Driver) { if (nls.control === this.id) { if (nls.property === 'NAME') { this.label = this.selectValue(this.label, nls.value, nls.nlsId); } } } else if (nls.type === NLSRecordType.Generic) { if (nls.control === this.id) { if (nls.property === 'NAME') { this.label = this.selectValue(this.label, nls.value, nls.nlsId); } } } } public override toJSON() { return { id: this.id, name: this.name, label: this.label, hidden: this.hidden, optional: this.optional, readonly: this.readonly, editorId: this.editorId, dataType: this.dataType, settable: this.settable }; } // #endregion Public Methods (2) } export class CommandDefinition extends NodeMemberDefinition<string> { // #region Properties (2) public initialValue?: Driver.Type; public parameters?: ParameterDefinition[]; // #endregion Properties (2) // #region Constructors (1) constructor(def: AcceptCommandDef, classDef: NodeClassDefinition<any>) { super(classDef); this.id = def.id; if (def.p) { this.parameters = []; for (const p of toArray(def.p)) { if (p.id === '') { this.editorId = p.editor; this.initialValue = p.init as Driver.Type; this.optional = p.optional === 'T'; p.id = 'value'; } this.parameters.push(new ParameterDefinition(p, classDef)); } } } // #endregion Constructors (1) // #region Public Getters And Setters (1) public override get name(): string { if (Object.values(this.classDef.drivers).find((d) => d.name === super.name)) { return 'update' + capitalize(super.name); } return super.name; } // #endregion Public Getters And Setters (1) // #region Public Methods (4) public override applyEditorDef(e: EditorDef): void { super.applyEditorDef(e); for (const p of this.parameters ?? []) { p.applyEditorDef(e); } } public override applyIndexDef(e: { [x: string]: { [y: number]: string } }): void { super.applyIndexDef(e); for (const p of this.parameters ?? []) { p.applyIndexDef(e); } } public applyNLSRecord(nls: NLSGenericRecord | NLSCommandRecord | NLSCommandParameterRecord) { if (nls.type === NLSRecordType.Command) { if (nls.control === this.id) { if (nls.property === 'NAME') { this.label = this.selectValue(this.label, nls.value, nls.nlsId); } } } else if (nls.type === NLSRecordType.Generic) { if (nls.control === this.id) { if (nls.property === 'NAME') { this.label = this.selectValue(this.label, nls.value, nls.nlsId); } } if (this.parameters && this.parameters.find((p) => p.id == nls.control)) { let p = this.parameters.find((p) => p.id == nls.control); p.applyNLSRecord(nls); } } else if (nls.type === NLSRecordType.CommandParameter || nls.type === NLSRecordType.CommandParameterNLS) { if (this.parameters && this.parameters.find((p) => p.id == nls.control)) { let p = this.parameters.find((p) => p.id == nls.control); p.applyNLSRecord(nls); } } } // applyEditorDef(e: EditorDef) { // if (e.id === this.editorId) { // var d = {}; // for (const rangeDef of toArray(e.range)) { // if ("subset" in rangeDef) { // d[rangeDef.uom] = { indexId: rangeDef.nls, values: rangeDef.subset.split(',') }; // } else { // d[rangeDef.uom] = { min: rangeDef.min, max: rangeDef.max, step: rangeDef.step, prec: rangeDef.prec }; // } // } // this.dataType = d; // } // public override toJSON() { return { id: this.id, name: this.name, label: this.label, hidden: this.hidden, editorId: this.editorId, dataType: this.dataType, optional: this.optional, parameters: this.parameters, initialValue: this.initialValue }; } // #endregion Public Methods (4) } export class ParameterDefinition extends NodeMemberDefinition<string> { // #region Properties (1) public initialValue: Driver.Type; // #endregion Properties (1) // #region Constructors (1) constructor(def: ParamDef, classDef: NodeClassDefinition<any>) { super(classDef, def); this.id = def.id; this.editorId = def.editor; this.initialValue = def.init as Driver.Type; this.optional = def.optional === 'T'; } // #endregion Constructors (1) // #region Public Methods (2) public applyNLSRecord(nls: NLSCommandParameterRecord | NLSGenericRecord) { if (nls.property === 'NAME') { this.label = this.selectValue(this.label, nls.value, nls.nlsId); } } public override toJSON() { return { id: this.id, name: this.name, label: this.label, hidden: this.hidden, editorId: this.editorId, dataType: this.dataType, optional: this.optional, initialValue: this.initialValue }; } // #endregion Public Methods (2) // applyEditorDef(e: EditorDef) { // if (e.id === this.editorId) { // var d = {}; // for (const rangeDef of toArray(e.range)) { // if ("subset" in rangeDef) { // d[rangeDef.uom] = { indexId: rangeDef.nls, values: rangeDef.subset }; // } else { // d[rangeDef.uom] = { min: rangeDef.min, max: rangeDef.max, step: rangeDef.step, prec: rangeDef.prec }; // } // } // this.dataType = d; // } // } } export class EventDefinition extends NodeMemberDefinition<string> { // #region Constructors (1) constructor(def: SendCommandDef, classDef: NodeClassDefinition<any>) { super(classDef); this.id = def.id; } // #endregion Constructors (1) // #region Public Methods (1) public applyNLSRecord(nls: NLSCommandRecord | NLSGenericRecord) { if (nls.type === NLSRecordType.Command) { if (nls.control === this.id) { if (nls.property === 'NAME') { this.label = this.selectValue(this.label, nls.value, nls.nlsId); } } } else if (nls.type === NLSRecordType.Generic) { if (nls.control === this.id) { if (nls.property === 'NAME') { this.label = this.selectValue(this.label, nls.value, nls.nlsId); } } } } // #endregion Public Methods (1) } const NodeClassMap = new Map<Family, { [x: string]: NodeClassDefinition<Family> }>(); export namespace NodeClassDefinition { export const Map = NodeClassMap; export function generate<T extends Family>(family: T, nodeDefs: NodeDef[]): { [x: string]: NodeClassDefinition<T> } { if (nodeDefs) { var n = buildNodeClassDefinitions(nodeDefs, family); NodeClassMap.set(family, n); return n; } } export function load(path: string) { for (const file of new Set(fs.readdirSync(path + '/generated').concat(fs.readdirSync(path + '/overrides')))) { let fam = file.replace('.json', ''); let family = Family[fam as keyof typeof Family]; let nodeClassDefs: { [x: string]: NodeClassDefinition<Family>; } = {}; if (fs.existsSync(`${path}/generated/${fam}.json`)) { nodeClassDefs = JSON.parse(fs.readFileSync(`${path}/generated/${fam}.json`, 'utf8')) as { [x: string]: NodeClassDefinition<Family>; }; } if (fs.existsSync(`${path}/overrides/${fam}.json`)) { nodeClassDefs = merge( nodeClassDefs, JSON.parse(fs.readFileSync(`${path}/overrides/${fam}.json`, 'utf8')) as { [x: string]: NodeClassDefinition<Family>; } ); } delete nodeClassDefs['$schema']; populateClassDefinitions(nodeClassDefs); NodeClassMap.set(family, nodeClassDefs); } return NodeClassMap; function populateClassDefinitions(nodeClassDefs: { [x: string]: NodeClassDefinition<Family> }) { for (var id in nodeClassDefs) { if (id != '$schema') { let entry = nodeClassDefs[id]; for (var driver of Object.values(entry.drivers)) { driver.classDef = entry; } for (var command of Object.values(entry.commands)) { command.classDef = entry; if (command.parameters) for (var param of command.parameters) { param.classDef = entry; } } } } } } export function save(path: string) {} }