UNPKG

homebridge-smartsystem

Version:

SmartServer (Proxy Websockets to TCP sockets, Smappee MQTT, Duotecno IP Nodes, Homekit interface)

992 lines (841 loc) 32.1 kB
import { CommRecord, Message, Sanitizers, NodeInfo, UnitInfo, DBInfo, WriteError, hex, NodeType, UnitType, UnitExtendedType, UnitState, two } from "./types"; import { debug, log, err } from "./logger"; import { Master } from "./master"; import { SmartSocket } from "./smartsocket"; import { EventEmitter } from "events"; // Duotecno master IP protocol implementation // Johan Coppieters // // Dec 2018 - v1 - first version based on 1 smartbox // Mar 2019 - v2 - rewrite to support multiple masters // // Dec 2019 - v3 - for app side only // May-Nov 2021 - v4 - changes for pro-app // Nov 2022 - Register and merge with gateway /////////////////////////// // Commands + attributes // /////////////////////////// enum Cmd { SetSensorValue = 7, Internal = 9, // smartsystem defined (not Duotecno) SetBasicAudio = 159, SetExtendedAudio = 208, SetAVMatrix = 202, SetSwitch = 163, SetDimmer = 162, SetControl = 168, SetMotor = 182, SetSensor = 136, SetDateTime = 170, Login = 214, Heartbeat = 215, DatabaseInfo = 209, SetSchedule = 217, reqSchedule = 218, reqNodeManagement = 220, Register = 224 } // for Set Switch/Dimmer/Control/Motor/Sensor = 162, 163, 168, 182, 136 const reqDim = 3; const reqOff = 9; const reqOn = 10; // for Login = 214 const reqDisconnect = 0; const reqConnect = 3; // for DatabaseInfo = 209; const reqDBInfo = 0; const reqNodeInfo = 1; const reqUnitInfo = 2; const reqUnitStatus = 3; // for reqNodeMgt, versions et al export enum reqNodeAttributes { masterSupported = 0, isMaster = 4, nodeInfo = 5, nodeVersion = 6, nodeProtocol = 7 } export function cmdName(cmd: number | string) { return Cmd[cmd] || "cmd"+cmd; } type CmdSet = { cmd: number, method: number, value?: number | boolean, message?: Message, reqStatus?: boolean } export enum TempPreset { off = -1, sun = 0, halfsun = 1, hsun = 1, moon = 2, halfmoon = 3, hmoon = 3 } ////////////////////// // Received results // ////////////////////// export enum Rec { // return info from cmdDatabaseInfo DBInfo = 0, NodeInfo = 1, UnitInfo = 2, Internal = 9, // smartsystem defined (not Duotecno) ErrorMessage = 17, ConnectStatus = 67, AudioStatus = 23, AudioExtendedStatus = 70, TimeDateStatus = 71, HeartbeatStatus = 72, ScheduleStatus = 73, NodeMgtInfo = 74, Register = 77, // return info from recDBInfo Info = 64, // return info from reqUnitStatus Mood = 4, Dimmer = 5, Switch = 6, Sensor = 7, Motor = 38, Macro = 69 }; export function recName(rec: number) { return Rec[rec] || "rec"+rec; } ///////////////////////// // Node in the network // ///////////////////////// export class Node { master: Master; active: boolean; name: string; index: number; logicalAddress: number; physicalAddress: number; type: NodeType; flags: number; nrUnits: number; units: Array<Unit>; constructor(master: Master, params: NodeInfo) { this.master = master; Sanitizers.nodeInfo(params, this); this.units = []; // remove | in names let separ = this.name.indexOf("|"); this.name = (separ < 0) ? this.name : this.name.substring(0, separ) + " " + (this.name.substring(separ+1)); } inMultiNode() { return this.master.inMultiNode(); } typeName(): string { switch(this.type) { case NodeType.kStandardNode: return "Standard"; case NodeType.kGatewayNode: return "Gateway"; case NodeType.kModemNode: return "Modem"; case NodeType.kGUINode: return "GUI"; default: return "Unknown node type (" + this.type + ")"; } } getName(): string { return this.name; } getSort(): string { return this.getName().toLowerCase(); } getNumber(): string { return hex(this.logicalAddress); } getDescription(): string { return this.getName() + ", active: " + this.active + ", type: " + this.typeName() + ", node: " + this.getName(); } findUnit(logicalAddress: number): Unit { return this.units.find(u => u && (u.logicalAddress === logicalAddress)); } findUnitIndex(logicalAddress: number): number { return this.units.findIndex(u => u && (u.logicalAddress === logicalAddress)); } } ///////////////////////// // Unit within a Node // ///////////////////////// export class Unit { node: Node; active: boolean; used: boolean; name: string; group: number = 0; index: number; logicalNodeAddress: number; logicalAddress: number; type: UnitType; extendedType: UnitExtendedType; flags: number; displayName: string; value: number | boolean; status: number; resetTimer = null; // Temperature stuff (values in x10 degree Celcius) preset: number; // 0=sun, 1=half sun, 2=moon, 3=half moon, -1=off sun: number; hsun: number; moon: number; hmoon: number; temp: number; constructor(node, params: UnitInfo, moodName: string = "mood") { this.node = node; Sanitizers.unitInfo(params, this); this.extendedType = this.extendedType || this.calcExtendedType(); this.name = this.name || this.getSerialNr(); // delete the "<" and ">" for power meters // if (this.extendedType === UnitExtendedType.kPower) // this.name = this.name.substring(1); // make a name for homekit, without the | but add § is 'specials' to add "sfeer", etc... // if the display name is empty make a N[nodeAdr]-U[unitAdr] name. // delete all type modifiers ( $, * and ! ) this.displayName = this.displayName || this.name.replace(/\|/g, this.hasSpecials() ? (" "+moodName+" ") : " ").replace(/\$|\*|\!/g, '') || this.getSerialNr(); } hasSpecials(): boolean { let special = this.name.indexOf("|20"); if (special < 0) special = this.name.indexOf("|50"); if (special < 0) special = this.name.indexOf("|90"); if (special < 0) special = this.name.indexOf("|OFF"); return special >= 0; } isUnit(unit: Unit): boolean; isUnit(master: string, port: number, nodeLogicalAddress: number, unitLogicalAddress: number): boolean; isUnit(master: Unit | string, port?: number, nodeLogicalAddress?: number, unitLogicalAddress?: number): boolean { if (master instanceof Unit) { const unit = master; return ((this.node.master.same(unit.node.master)) && (this.node.logicalAddress == unit.node.logicalAddress) && (this.logicalAddress == unit.logicalAddress)); } else { /* if (typeof master === "string") */ return ((this.node.master.same(master, port)) && (this.node.logicalAddress == nodeLogicalAddress) && (this.logicalAddress == unitLogicalAddress)); } } sameValue(value): boolean { if (this.type === UnitType.kSwitchingMotor) return (((this.value == UnitState.kOpening) && (value == 4)) || ((this.value == UnitState.kClosing) && (value == 5)) || ((this.value as number <= UnitState.kOpen) && (value == 3))); else return this.value == value; } typeName(): string { switch (this.getType()) { case UnitExtendedType.kDimmer: return 'Dimmer'; case UnitExtendedType.kSwitch: return 'Switch/Relay'; case UnitExtendedType.kLightbulb: return 'Lightbulb'; case UnitExtendedType.kInput: return 'Control input'; case UnitExtendedType.kTemperature: return 'Temperature sensor'; case UnitExtendedType.kPower: return 'Power meter'; case UnitExtendedType.kExtendedAudio: return 'Extended audio'; case UnitExtendedType.kMood: return 'Virtual mood'; case UnitExtendedType.kCondition: return 'Condition'; case UnitExtendedType.kSwitchingMotor: return 'Switch motor'; case UnitExtendedType.kGarageDoor: return 'Garagedoor'; case UnitExtendedType.kDoor: return 'Door'; case UnitExtendedType.kLock: return 'Lock'; case UnitExtendedType.kUnlocker: return 'Unlocker'; case UnitExtendedType.kAudio: return 'Basic audio'; case UnitExtendedType.kAV: return 'AV Matrix'; case UnitExtendedType.kIRTX: return 'IRTX'; case UnitExtendedType.kVideo: return 'Video multiplexer'; default: return 'Unknown unit type (' + this.type + ')'; } } getName(): string { return this.name; } getDisplayName(): string { return this.displayName; } getNumber(): string { return this.node.getNumber() + ";" + hex(this.logicalAddress); } getSort(): string { const name = this.getName().toLowerCase(); switch(this.type) { case UnitType.kTemperature: return "01|" + name; case UnitType.kSwitchingMotor: return "02|" + name; case UnitType.kDimmer: return "03|" + name; case UnitType.kSwitch: return "04|" + name; case UnitType.kMood: return "09|" + name; case UnitType.kInput: return "10|" + name; // case UnitType.kPower: return "11|" + name; case UnitType.kExtendedAudio: "12|" + name; case UnitType.kAudio: return "13|" + name; case UnitType.kAV: return "14|" + name; case UnitType.kVideo: return "15|" + name; case UnitType.kIRTX: return "19|" + name; default: return "99|" + name; } } getType(): UnitExtendedType { return this.extendedType; } calcExtendedType(): UnitExtendedType { // General idea extention on DuoTecno's types // // $ -> kind of lock -> needs authentication // * -> toggle // // Extension on Duotecno's types // updown => // if name contains $ => "garagedoor" // if name contains * => "door" // else => "window-covering" // mood => // if name contains $ => "unlock", locks again after 1.2 sec // if name contains * => permanent locked=on/unlocked=off // else => "mood" (turns of 1.2 seconds after being turned on) // switch => // if name contains $ => "lock" // if name contains * => "switch" (also still works with "stc", "Stc", "STC", stk", "STK" and "Stk") // else => "lightbulb" // //////////// // Switch // //////////// // Switch -> with * or STK -> Switch if ((this.type === UnitType.kSwitch) && ((this.name.indexOf("STK") >= 0) || (this.name.indexOf("stk") >= 0) || (this.name.indexOf("Stk") >= 0) || (this.name.indexOf("STC") >= 0) || (this.name.indexOf("stc") >= 0) || (this.name.indexOf("Stc") >= 0) || (this.name.indexOf("*") >= 0))) return UnitExtendedType.kSwitch; // Switch -> with $ -> Door if ((this.type === UnitType.kSwitch) && (this.name.indexOf("$") >= 0)) return UnitExtendedType.kLock; // Switch -> default -> LightBulb if (this.type === UnitType.kSwitch) return UnitExtendedType.kLightbulb; ////////////////// // Temp / Power // ////////////////// //if ((this.type === UnitType.kTemperature) && // ((this.name[0] === "<") || (this.name[0] === ">"))) // return UnitExtendedType.kPower; ///////////// // Up/Down // ///////////// // UpDown -> with $ -> GarageDoor if ((this.type === UnitType.kSwitchingMotor) && (this.name.indexOf("$") >= 0)) return UnitExtendedType.kGarageDoor; // UpDown with * -> Door if ((this.type === UnitType.kSwitchingMotor) && (this.name.indexOf("*") >= 0)) return UnitExtendedType.kDoor; // UpDown -> default -> WindowCovering if (this.type === UnitType.kSwitchingMotor) return UnitExtendedType.kSwitchingMotor; /////////// // Moods // /////////// // Mood -> with $ -> Lock (re-closes after 1.2 secs) if ((this.type === UnitType.kMood) && (this.name.indexOf("$") >= 0)) return UnitExtendedType.kUnlocker; // Mood -> with * -> Mood with state if ((this.type === UnitType.kMood) && (this.name.indexOf("*") >= 0)) return UnitExtendedType.kCondition; // Mood -> default -> Mood (turn off after 1.2 secs) if (this.type === UnitType.kMood) return UnitExtendedType.kMood; /////////////////////// // All other default // /////////////////////// return <UnitExtendedType><unknown>this.type; } getSerialNr(): string { if (this.inMultiNode) return this.node.getName() + "-N" + this.logicalNodeAddress + "-U" + this.logicalAddress; else return "N" + this.logicalNodeAddress + "-U" + this.logicalAddress; } getModelName(): string { return this.typeName() + " " + hex(this.node.logicalAddress)+ ";" + hex(this.logicalAddress); } isCtrl(): boolean { return this.isSwitch() || this.isDimmer() || this.isUpDown(); } isSwitch(): boolean { return (this.type === UnitType.kSwitch); } isMood(): boolean { return (this.type === UnitType.kMood); } isInput(): boolean { return (this.type === UnitType.kInput); } isTemperature(): boolean { return (this.type === UnitType.kTemperature); } isDimmer(): boolean { return (this.type === UnitType.kDimmer); } isUpDown(): boolean { return (this.type === UnitType.kSwitchingMotor); } async setPreset(preset: number, temp: number) { await this.node.master.setPreset(this, preset, temp); } async selectPreset(preset: number) { await this.node.master.selectPreset(this, preset); } async sensorOnOff(on: boolean) { await this.node.master.setTempOnOff(this, on); } async doIncDecPreset(inc: boolean) { await this.node.master.doIncDecPreset(this, inc); } async setSensorValue(temp: number) { await this.node.master.setSensorValue(this, temp); } async setSensorStatus(value: number) { await this.node.master.setSensorStatus(this, value); } inMultiNode() { return this.node.inMultiNode(); } async reqState(callback?: DeliverStatus): Promise<void> { await this.node.master.requestUnitStatus(this); if (callback) Protocol.addSubscriber(callback, this); } async setState(value) { await this.node.master.setUnitStatus(this, value); } getDispayState(): string { switch(this.getType()) { case UnitExtendedType.kDimmer: return ((this.status) ? 'on' : 'off') + ' (' + this.value + '%)'; case UnitExtendedType.kSwitch: case UnitExtendedType.kLightbulb: return (this.status) ? 'on' : 'off'; case UnitExtendedType.kInput: return (this.status) ? 'on' : 'off'; case UnitExtendedType.kPower: const aval = Math.abs(this.sun * 100 * 100 * 100 + this.hsun * 100 * 100 + this.moon * 100 + this.hmoon); return (isNaN(aval) ? "-" : ((aval < 10000) ? (aval + " W") : (Math.round(aval / 1000) + "." + two(Math.round(aval % 1000 / 100)) + " kW") )); case UnitExtendedType.kTemperature: return isNaN(<number>this.value) ? "-" : ((<number>this.value / 10.0) + 'C'); case UnitExtendedType.kCondition: case UnitExtendedType.kMood: return (this.status) ? 'on' : 'off'; case UnitExtendedType.kLock: return (this.status) ? 'locked' : 'unlocked'; case UnitExtendedType.kUnlocker: return (this.status) ? 'unlocking' : 'locked'; case UnitExtendedType.kGarageDoor: case UnitExtendedType.kDoor: case UnitExtendedType.kSwitchingMotor: if (this.status === UnitState.kOpening) { return 'opening'; } if (this.status === UnitState.kClosing) { return 'closing'; } if (this.status === UnitState.kOpen) { return 'open'; } if (this.status === UnitState.kClosed) { return 'closed'; } if (this.status === UnitState.kStopped) { return 'stopped'; } } return (typeof this.status != "undefined") ? this.status.toString() : 'unknown'; } getDescription() { return this.getDisplayName() + ", active: " + this.active + ", type: " + this.typeName() + ", status: " + this.status + ", value: " + this.value + " -> " + this.getDispayState(); } } ///////////////////////////////////// // IP node protocol implementation // ///////////////////////////////////// type DeliverStatus = (unit: Unit) => void; type ValueSubscribtion = {deliver: DeliverStatus, unit: Unit}; // callbacks, waiting to be called when a status for them arrives const subscribers: Array<ValueSubscribtion> = []; export const Protocol = { emitter: null, setEmitter(emitter: EventEmitter) { this.emitter = emitter; }, ///////////////// // Subscribers // ///////////////// alertSubscriber(unit: Unit) { const inx = subscribers.findIndex(vs => vs.unit.isUnit(unit)); if (inx >= 0) { subscribers[inx].deliver(unit); subscribers.splice(inx, 1); } }, addSubscriber(deliver: DeliverStatus, unit: Unit) { subscribers.push({deliver, unit}); }, //////////////////// // Helper methods // //////////////////// getStr: function(arr: Array<number>, at: number) { return arr.slice(at+1, at+arr[at]+1) .map(val => String.fromCharCode(val)) .join(""); }, makeWord: function(arr: Array<number>, at: number) { return arr[at+0] * 256 + arr[at+1]; }, makeSignedWord: function(arr: Array<number>, at: number) { if (arr[at+0] > 127) return (arr[at+0]-255) * 256 + (arr[at+1]-255) - 1; else return arr[at+0] * 256 + arr[at+1]; }, makeLong: function(arr: Array<number>, at: number) { return arr[at+0] * 256 * 256 * 256 + arr[at+1] * 256 * 256 + arr[at+2] * 256 + arr[at+3]; }, makeSigned: function(arr: Array<number>, at: number) { if (arr[at+0] > 127) return (arr[at+0]-255) * 256 * 256 * 256 + (arr[at+1]-255) * 256 * 256 + (arr[at+2]-255) * 256 + (arr[at+3]-255) - 1; else return arr[at+0] * 256 * 256 * 256 + arr[at+1] * 256 * 256 + arr[at+2] * 256 + arr[at+3]; }, signedToArray: function(value: number) { if (value < 0) return [256 + Math.floor(value / 256 / 256 / 256), 256 + Math.floor(value / 256 / 256) % 256, 256 + Math.floor(value / 256) % 256, 256 + Math.floor(value) % 256] else return [Math.floor(value / 256 / 256 / 256), Math.floor(value / 256 / 256) % 256, Math.floor(value / 256) % 256, Math.floor(value) % 256]; }, signedWordToArray: function(value: number) { if (value < 0) return [256 + Math.floor(value / 256) % 256, 256 + Math.floor(value) % 256]; else return [Math.floor(value / 256) % 256, Math.floor(value) % 256]; }, wordToArray: function(value: number) { return [Math.floor(value / 256) % 256, Math.floor(value) % 256]; }, isStatus: function(cmd: number): boolean { return (cmd === Rec.Mood) || (cmd === Rec.Dimmer) || (cmd === Rec.Switch) || (cmd === Rec.Sensor) || (cmd === Rec.Motor) || (cmd === Rec.Macro); }, ////////////////////////// // Code to String stuff // ////////////////////////// translateError: function(err: Message): string { if (err[0] != Rec.ErrorMessage) return "received unexpected data: " + err; switch(err[1]) { case 11: return "Wrong object method received for " + err[2] + "/" + err[3]; case 12: return "Wrong Message Code received: " + err[2]; case 18: return "This function can only be executed when this node is the master"; case 128: return "The node database is not ready"; case 129: return "Node " + err[2] + " could not be found in the database"; case 130: return "Wrong node index: " + err[2]; case 131: return "Unit " + err[2] + " with address " + err[3] + " could not be found in the database"; case 132: return "Wrong unit index " + err[3] + " for this node " + err[2]; case 133: return "Unit " + err[3] + " of node " + err[2] + " is of a different type"; case 140: return "The requested operation is not allowed"; case 141: return "The requested operation is not allowed because a wrong access code is used"; case 142: return "The requested operation is not implemented in this software version"; default: return "Unknown error"; } }, //////////////////// // Socket methods // //////////////////// write: function(socket: SmartSocket, data: Message | string): WriteError { let cmd: string | number = parseInt(<string> data[0]); if (isNaN(cmd)) cmd = <string> data[0]; if (data instanceof Array) { data = data.join(","); } if (typeof data === "string") { // if no enclosing "[...]", add them if (data[0] != "[") data = "[" + data + "]"; log("protocol", "sending: " + cmdName(cmd) + " - " + data); try { // append a LF char and send socket.write(data+String.fromCharCode(10)); return WriteError.writeOK; } catch(e) { err("protocol", "error sending through socket " + e.message); return WriteError.writeFatal; } } else { throw(new Error("wrong data type for sending")); } }, ////////////////////////////// // Handle incoming data // // strip [] // // convert to array // // convert chars to ints // ////////////////////////////// nextMessage: function(buffer: string): CommRecord { // pre return result const nextRec: CommRecord = { rest: buffer, isStatus: false, message: null, cmd: 0 }; // no "start-of-data" -> discard buffer || else -> trim buffer const begin = buffer.indexOf("["); if (begin < 0) { nextRec.rest = ""; } else if (begin != 0) { nextRec.rest = buffer.substring(begin); } // we either have valid start data or nothing if (nextRec.rest.length > 0) { // do we have an "end-of-data" in our buffer let end = nextRec.rest.indexOf("]"); // if no end-of-data was found: // leave it in the buffer and hope more data will arrive soon //TODO: set up a timer that clears the buffer if nothing comes through if (end >= 0) { // fetch the first available message (discard the [ and ]) const msg = nextRec.rest.substring(1, end); // delete the used message from the input buffer // if there, also delete the trailing LF (0x0A) if ((end <= nextRec.rest.length) && (nextRec.rest.charCodeAt(end+1) === 0x0A)) end++; nextRec.rest = nextRec.rest.substring(end+1); // convert to array and turn strings into numbers if IP command nextRec.message = msg.split(",").map(c => parseInt(c)); // get the first byte to see what kind of incoming data nextRec.cmd = <number>nextRec.message[0]; nextRec.isStatus = this.isStatus(nextRec.cmd); debug("protocol", "processing: " + (nextRec.isStatus ? "status -> " : "") + msg); } } return nextRec; }, buildLogin: function(password: string): Message { password = password || ""; return [Cmd.Login, reqConnect, password.length, ...password.split('').map(c => c.charCodeAt(0))]; }, buildDisconnect: function(): Message { return [Cmd.Login, reqDisconnect]; }, buildHeartbeat: function(): Message { return [Cmd.Heartbeat, 1]; }, ///////////////////////////////////// // Collect info of all found nodes // ///////////////////////////////////// buildDBInfo: function(): Message { return [Cmd.DatabaseInfo, reqDBInfo]; }, buildNodeInfo: function(nodeInx: number): Message { return [Cmd.DatabaseInfo, reqNodeInfo, nodeInx]; }, buildUnitInfo: function(node: Node, unitInx: number) { return [Cmd.DatabaseInfo, reqUnitInfo, node.logicalAddress, unitInx]; }, buildRequestUnitStatus: function(node: Node, unit: Unit): Message { return [Cmd.DatabaseInfo, reqUnitStatus, node.logicalAddress, unit.logicalAddress, unit.type]; }, buildRequestSchedule(): Message { return [Cmd.reqSchedule, 0]; }, buildRequestNodeMgt(method = reqNodeAttributes.nodeProtocol): Message { return [Cmd.reqNodeManagement, method]; }, getCmdAndMethod: function(unit: Unit, value: number | boolean): CmdSet { switch (unit.type) { case UnitType.kDimmer: if (typeof value === "boolean") return { cmd: Cmd.SetDimmer, method: (value) ? reqOn : reqOff }; else if (value <= 0) return { cmd: Cmd.SetDimmer, method: reqOff }; else return { cmd: Cmd.SetDimmer, method: reqDim, value: Math.max(Math.min(value, 99),1) }; case UnitType.kSwitch: return { cmd: Cmd.SetSwitch, method: (value) ? 3 : 2 }; case UnitType.kInput: case UnitType.kMood: if (value as number < 0) return { cmd: Cmd.SetControl, method: 2 }; // short pulse else return { cmd: Cmd.SetControl, method: 3, value: (value) ? 1 : 0 }; // long event + 0/1 case UnitType.kSwitchingMotor: return { cmd: Cmd.SetMotor, method: <number>value }; // 5 close, 4 open, 3 is stop case UnitType.kTemperature: return { cmd: Cmd.SetSensor, method: 13 /* select preset */, value }; case UnitType.kExtendedAudio: case UnitType.kAudio: case UnitType.kAV: case UnitType.kIRTX: case UnitType.kVideo: default: // "Unknown unit type (" + unit.type + ")"; this.err("setting " + unit.type + " not yet implemented"); return { cmd: 0, method: 0, value: 0} } }, buildSetCmd: function(node: Node, unit: Unit, value: number | boolean): CmdSet { let params = this.getCmdAndMethod(unit, value); if (params.cmd) { params.message = [params.cmd, params.method, node.logicalAddress, unit.logicalAddress]; if (typeof params.value != "undefined") params.message.push(params.value); // some need a requestStatus afterwards params.reqStatus = ((params.cmd === Cmd.SetDimmer) && (params.method === reqDim)); } return params; }, /* Temperature / Presets */ buildSelectPreset(node: Node, unit: Unit, preset: number): Message { return [Cmd.SetSensor, 13, node.logicalAddress, unit.logicalAddress, preset]; }, buildSetSensor(node: Node, unit: Unit, value: number): Message { return [Cmd.SetSensorValue, 10, node.logicalAddress, unit.logicalAddress, UnitType.kTemperature, ...this.wordToArray(value)]; }, buildSetPreset(node: Node, unit: Unit, preset: number, value: number): Message { return [Cmd.SetSensor, 1, node.logicalAddress, unit.logicalAddress, preset, ...this.wordToArray(value)]; }, buildIncDecPreset(node: Node, unit: Unit, inc: boolean): Message { return [Cmd.SetSensor, (inc) ? 5 : 6, node.logicalAddress, unit.logicalAddress]; }, buildSensorOnOff(node: Node, unit: Unit, on: boolean): Message { return [Cmd.SetSensor, 3, node.logicalAddress, unit.logicalAddress, (on) ? 1 : 0]; }, /* Schedule commands */ buildSendSchedule(schedule: number): Message { return [Cmd.SetSchedule, 0, Math.max(0, Math.min(3, schedule))]; }, buildRegister(register: number, value?: number) { if (typeof value != "undefined") { // set register return [Cmd.Register, 1, ...this.wordToArray(register), ...this.signedToArray(value)]; } else { // get register return [Cmd.Register, 2, ...this.wordToArray(register)]; } }, /////////////////// // Received info // /////////////////// receiveStatus: function(next: CommRecord, unit: Unit) { let kind = "-"; if (next.cmd === Rec.Sensor) { // sensor -> value unit.value = this.makeWord(next.message, 9); // 10x current temperature unit.status = next.message[7]; // 0=idle, 1=heating, 2=cooling unit.preset = (next.message[6]) ? next.message[8] : -1; // 0=sun, 1=half sun, 2=moon, 3=half moon, -1 = off unit.sun = this.makeWord(next.message, 11); // 10x temperature unit.hsun = this.makeWord(next.message, 13); // 10x temperature unit.moon = this.makeWord(next.message, 15); // 10x temperature unit.hmoon = this.makeWord(next.message, 17); // 10x temperature debug("protocol", "received status - " + unit.getDisplayName() + ", temperature = " + <number>unit.value / 10.0 + ", sun = " + unit.sun); kind = "S"; // Dimmers, switches and moods have // - status (0=off,1=on,2=pir-on) // = value (true/false for switches and moods, 1-99 for dimmers) } else if (next.cmd === Rec.Switch) { // switch -> boolean unit.status = next.message[6]; unit.value = (next.message[6] > 0); debug("protocol", "received status - switch = " + unit.value); kind = "S"; } else if (next.cmd === Rec.Dimmer) { // dimmer -> 0 .. 99 unit.status = next.message[6]; unit.value = next.message[7]; debug("protocol", "received status - dimmer -> value=" + unit.value + " / status=" + unit.status); kind = "S"; } else if (next.cmd === Rec.Mood) { // control -> boolean unit.status = next.message[6]; unit.value = (next.message[6] != 0); debug("protocol", "received status - mood = " + unit.value); kind = "S"; } else if (next.cmd === Rec.Motor) { // motor -> boolean/status // [38,0,252,104,8,0,0] // 0 = stopped, 1 stopped/down, 2 = stopped/up, 3 = busy/down, 4 = busy/up unit.status = next.message[6]; unit.value = next.message[6]; debug("protocol", "received status - motor = " + unit.value); kind = "S"; } else if (next.cmd === Rec.Macro) { // = EV_UNITMACROCOMMANDO // examples: On 50%: [69,0,NodeAddress,UnitAddress,6,1,0,50] // Off: [69,0,NodeAddress,UnitAddress,6,0,0,0] -> don't touch dimmer value // unit_type_duoswitch -> Event = 32, state 0, 1, 2 // STATUS (Event: 7 = dimmer, 5 = switch, on = 1, off = 0) if (((next.message[4] == 7) || (next.message[4] == 5)) && (next.message[5] == 1)) { // PIR ON -- override status value with "2" (our PIR ON) unit.status = 2; } else if ((next.message[4] < 8) || (next.message[4] == 10)) { // ON/OFF messages, including sensor types (off/heating/cooling) unit.status = next.message[5]; } // VALUE // only change dim value when state = 1 (ON, PIR ON, DIM STOP) for Dimmers and IRTX + for any setpoint event if ((((next.message[4] === 1) || (next.message[4] === 6) || (next.message[4] === 7)) && (next.message[5] === 1)) || (next.message[4] === 11)) { unit.value = this.makeWord(next.message, 6); } else if (next.message[4] === 32) { // unit_type_duoswitch -> Event = 32 // [69,0,252,104,32,2,0,0] // state = 0 -> stop, state = 1 -> up, state = 2 -> down unit.value = 10 + next.message[5]; unit.status = next.message[5]; } debug("protocol", "received status - macro -> value=" + unit.value + " / status=" + unit.status); kind = "M"; } this.alertSubscriber(unit); this.emitter.emit('update', unit, kind); }, makeDBInfo(res: Message): DBInfo { return { nrNodes: res[2] }; }, makeNodeInfo(res: Message): NodeInfo { let name = this.getStr(res, 8); let offset = name.length; return { name: name, index: res[2], // should be == nodeInx logicalAddress: res[3], physicalAddress: this.makeLong(res, 4), nrUnits: res[offset+9], type: res[offset+10], flags: res[offset+11] } }, makeUnitInfo(res: Message): UnitInfo { let name = this.getStr(res, 6); let offset = name.length; return { name, index: res[3], // should be == unitInx logicalNodeAddress: res[4], // difference with res[2] / logicalReqNodeAddress ? logicalAddress: res[5], type: res[offset+7], flags: res[offset+8] }; } };