homebridge-smartsystem
Version:
SmartServer (Proxy Websockets to TCP sockets, Smappee MQTT, Duotecno IP Nodes, Homekit interface)
884 lines (750 loc) • 27 kB
text/typescript
import { Node, Protocol, Unit, recName, Rec, TempPreset, reqNodeAttributes } from "./protocol";
import { MasterConfig, WriteError, CommRecord, Message, Sanitizers } from "./types";
import { log, debug } from "./logger";
import { System } from "./system";
import { Base } from "../server/base";
import { Q } from "./Q";
import { getSocket, SmartSocket } from "./smartsocket";
export class Master extends Base {
private system: System;
public nodes: Array<Node>;
public nrNodes: number;
public schedule: number;
public date: Date;
public lastHeartbeat: number;
public protocol: number = 0; // major * 100 + minor
public version: number = 0; // idem
public registers: {[register: number]: number} = {};
public isOpen: boolean;
public isLoggedIn: boolean;
private closing: boolean;
private resolveLogin = null;
private buffer: string;
private Q: Q;
config: MasterConfig;
private socket: SmartSocket;
private beater;
// command was sent, no response received yet
/* system.config: {
"socketserver": "akiworks.be",
"socketport": 9999,
"mood": "sfeer",
"cmasters": [
{
"address": "192.168.0.98",
"port": 5001,
"password": "duotecno",
"name": "Smartbox",
"active": false,
"debug": false
},
{
"address": "gm.coppieters.be",
"port": 5005,
"password": "duotecno",
"name": "IP Master GM",
"active": true,
"debug": false
}
],
"cunits": [
{
"active": "Y",
"group": 0,
"name": "rgb|Hue1",
"displayName": "Lamp - Brightness",
"type": 1,
"masterAddress": "gm.coppieters.be",
"masterPort": 5005,
"logicalNodeAddress": 6,
"logicalAddress": 38
},
{
"active": "Y",
"group": 0,
"name": "rgB|Hue1",
"displayName": "Lamp - Hue",
"type": 1,
"masterAddress": "gm.coppieters.be",
"masterPort": 5005,
"logicalNodeAddress": 6,
"logicalAddress": 37
},
{
"active": "Y",
"group": 0,
"name": "rGb|Hue1",
"displayName": "rGb Hue1",
"type": 1,
"masterAddress": "gm.coppieters.be",
"masterPort": 5005,
"logicalNodeAddress": 6,
"logicalAddress": 36
},
{
"active": "Y",
"group": 0,
"name": "Rgb|Hue1",
"displayName": "Rgb Hue1",
"type": 1,
"masterAddress": "gm.coppieters.be",
"masterPort": 5005,
"logicalNodeAddress": 6,
"logicalAddress": 35
},
{
"active": "Y",
"group": 0,
"name": "Sticks",
"displayName": "Sticks",
"type": 1,
"masterAddress": "gm.coppieters.be",
"masterPort": 5005,
"logicalNodeAddress": 6,
"logicalAddress": 34
},
{
"active": "Y",
"group": 0,
"name": "Vintage",
"displayName": "Vintage",
"type": 1,
"masterAddress": "gm.coppieters.be",
"masterPort": 5005,
"logicalNodeAddress": 6,
"logicalAddress": 33
},
{
"active": "Y",
"group": 0,
"name": "Driveway",
"displayName": "Driveway",
"type": 2,
"masterAddress": "gm.coppieters.be",
"masterPort": 5005,
"logicalNodeAddress": 6,
"logicalAddress": 40
},
{
"active": "Y",
"group": 0,
"name": "Trees",
"displayName": "Trees",
"type": 2,
"masterAddress": "gm.coppieters.be",
"masterPort": 5005,
"logicalNodeAddress": 6,
"logicalAddress": 39
}
]
}
*/
constructor(system: System, config: MasterConfig) {
super("master", Sanitizers.masterConfig(config));
this.Q = new Q();
// save my eco system
this.system = system;
// all nodes in this master
this.nodes = [];
this.nrNodes = 0;
this.schedule = 0;
this.date = null;
// connection to an IP node / smartbox
this.socket = null;
this.isOpen = false;
this.isLoggedIn = false;
this.closing = false;
// incoming data
this.buffer = "";
// init to v1.00
this.version = 100;
this.protocol = 100;
}
getName() {
return this.config.name || "master";
}
getSort() {
return this.getName().toLowerCase();
}
// overwrite to add IP address
log(msg: string) {
log("master", this.getAddress() + ": " + msg);
}
info(msg: string) {
debug("master", this.getAddress() + ": " + msg);
}
err(msg: string) {
log("master", this.getAddress() + ": " + msg);
}
writeConfig(config?: object, fn?: string) {
// overwrite: don't do anything, all is stored in the system-config
}
getConfig(): MasterConfig {
return this.config;
}
hasAddress(ip: string): boolean {
return this.config.address === ip;
}
getAddress(): string {
return this.config.address;
}
getPort(): number {
return this.config.port;
}
hasPort(port: number): boolean {
return this.config.port == port;
}
getURL(): string {
return this.config.address + ":" + this.config.port;
}
inMultiNode(): boolean {
return (this.nodes.length > 1);
}
same(master: Master | string, port?: number): boolean {
if (typeof master === "string") {
if (typeof port === "undefined") {
// master is probably url ip:port
const url = master.split(":");
master = url[0];
port = parseInt(url[1] || "5001");
}
return this.hasAddress(master) && this.hasPort(port);
} else {
return master && this.hasAddress(master.getAddress()) && this.hasPort(master.getPort());
}
}
/* ************* */
/* Communication */
/* ************* */
async open(cnt = 1) {
// open a socket to the master
// check to see if for some reason there is still a heartbeat around???
if (this.beater && this.closing) {
this.stopHeartbeat("We still had a heartbeat timer, but were asked for a new socket... ###############");
}
try {
// params 3-6: data message, closed, log and error functions
this.socket = await getSocket( this.config.address, this.config.port,
(msg) => {
this.handleData(msg);
},
(end) => {
this.isOpen = false;
this.isLoggedIn = false;
if (this.closing) {
this.log("end -> socket got closed as requested");
this.closing = false;
} else {
// our program didn't request the close, on next send it should automatically be reopened in Master.send()
this.log("end -> socket got unexpectedly disconnected -> will be reopened on next send ###############");
}
// stop sending heartbeats
this.stopHeartbeat("Stopping the heartbeat because the socket received and 'end'");
},
(logMessage) => {
this.log(logMessage);
},
(err) => {
this.err(err);
// on error restart the connection... you never know
this.forceClose();
}
);
this.isOpen = true;
this.closing = false;
this.log("master opened -> " + this.config.address);
// close the socket on timeouts, normally it should be automatically reopened
if (this.socket) this.socket.addListener("timeout", () => this.forceClose());
} catch (e) {
this.log("Open socket on -> " + this.config.address + " had an error: " + JSON.stringify(e));
}
this.startHeartbeat();
}
async forceClose() {
try {
this.log("master force closed");
//console.log("*HB* master force closed");
this.isOpen = false;
this.isLoggedIn = false;
this.closing = false;
this.stopHeartbeat("Stopping heartbeat because we are force-closing the socket");
this.socket.destroy();
} catch (e) {
this.log("Error while force-closing socket: " + JSON.stringify(e));
}
}
stopHeartbeat(msg, dontShow = false) {
if (this.beater) {
clearInterval(this.beater);
this.beater = null;
this.lastHeartbeat = 0;
if (msg)
this.log("*HB* " + msg);
} else if (! dontShow) {
this.err("*HB* There was no heartbeat when receiving: " + msg);
}
}
startHeartbeat() {
// setup a heartbeat
const kInterval = 30 * 1000;
if (this.beater) clearInterval(this.beater);
this.beater = setInterval(() => {
this.info("*HB* heartbeat time at " + new Date());
if (this.lastHeartbeat && ((new Date().getTime() - this.lastHeartbeat) > 2.5 * kInterval)) {
this.err("*HB* Didn't receive a heartbeat in 2.5 times our interval -> try to close manually");
if (this.isOpen) {
this.forceClose();
}
}
// always try to send a heartbeat, even if we're late.
this.send(Protocol.buildHeartbeat());
//console.log("*HB* send HB at " + new Date());
}, kInterval);
this.log("Heartbeat timer started")
}
async close(closing = true) {
if (this.isOpen) {
const message = Protocol.buildDisconnect();
try {
this.closing = closing;
this.log("master closing -> " + this.config.address);
await this.send(message);
// server will close the socket, no need to call socket.close()
} catch (err) {
this.err("Disconnect failure: " + err);
}
}
}
async login() {
return new Promise((resolve, reject) => {
try {
const message = Protocol.buildLogin(this.config.password)
Protocol.write(this.socket, message);
// to be called when logged in
this.resolveLogin = resolve;
} catch(err) {
this.resolveLogin = null;
this.err("Login call failed: " + err);
reject(false);
}
});
}
async send(message: Message) {
return this.Q.exec(async () => {
if (! this.isOpen) {
this.log("Opening socket because we receive a 'send' request, but socket was closed.");
await this.open();
if (! await this.login()) {
return WriteError.writeFatal;
}
}
return Protocol.write(this.socket, message);
});
}
handleData(message: string) {
// put the incoming data into a buffer and only use complete data
this.buffer += message;
while ((this.buffer.length > 0) && this.processData())
;
}
processData(): boolean {
// fetch next message and store the rest of unused data (if any)
const next = Protocol.nextMessage(this.buffer);
this.buffer = next.rest;
if (!next.cmd) {
return false;
} else {
// don't log all incoming (no time, no sensor status messages)
if ((next.cmd != Rec.TimeDateStatus) && (next.cmd != Rec.Sensor))
this.info("received -> " + recName(next.cmd) + " -> " + ((!next.message) ?
"--" : ((next.message instanceof Array) ? next.message.join(",") : next.message)));
this.Q.do();
if (next.isStatus) {
this.receiveStatus(next);
// non-unit specific info
} else if (next.cmd === Rec.TimeDateStatus) {
this.receiveDateTime(next.message);
} else if (next.cmd === Rec.Info) {
this.receiveInfo(next);
} else if (next.cmd === Rec.HeartbeatStatus) {
this.receiveHeartbeat(next.message);
} else if (next.cmd === Rec.ConnectStatus) {
this.receiveLogin(next.message);
} else if (next.cmd === Rec.ScheduleStatus) {
this.receiveSchedule(next.message);
} else if (next.cmd === Rec.NodeMgtInfo) {
this.receiveNodeMgtInfo(next.message);
} else if (next.cmd === Rec.Register) {
this.receiveRegister(next.message);
} else {
this.info("not handled: " + next.message);
}
return true;
}
}
///////////////////
// Login message //
///////////////////
receiveLogin(message: Message) {
this.isLoggedIn = (message[2] === 1);
if (this.resolveLogin) {
this.resolveLogin(this.isLoggedIn);
this.resolveLogin = null;
// if logged in, ask protocol version
if (this.isLoggedIn) {
this.send(Protocol.buildRequestNodeMgt(reqNodeAttributes.nodeProtocol));
}
} else {
this.err("unexpected ConnectStatus ?");
}
}
///////////////////
// Info messages //
///////////////////
receiveNodeMgtInfo(message: Message) {
if (message[1] === reqNodeAttributes.isMaster) {
this.log("NodeMgtInfo - master " + this.getName() + " is set to master: " + message[2]);
} else if (message[1] === reqNodeAttributes.nodeInfo) {
// should be the same as the one from the DBInfo (64)
this.log("NodeMgtInfo - node info for " + this.getName() + ": " + message);
} else if (message[1] === reqNodeAttributes.masterSupported) {
this.log("NodeMgtInfo - master " + this.getName() + " can be master: " + message[2]);
} else if (message[1] === reqNodeAttributes.nodeVersion) {
this.log("NodeMgtInfo - version " + this.getName() + ", version: " + message[2] + "." + message[3]);
this.version = message[2] * 100 + message[3];
console.log("*** version -> " + message + " -> " + this.version + " ***");
} else if (message[1] === reqNodeAttributes.nodeProtocol) {
this.log("NodeMgtInfo - protocol " + this.getName() + ", protocol: " + message[2] + "." + message[3]);
this.protocol = message[2] * 100 + message[3];
console.log("*** protocol -> " + message + " -> " + this.protocol + " ***");
} else {
this.err("dropped NodeMgtInfo info message = " + message[1] + ", message: " + message);
}
}
receiveRegister(message: Message) {
const register = Protocol.makeWord(message, 2);
const value = Protocol.makeSigned(message, 4);
if ((register >=0) && (register < 1024)) {
this.registers[register] = value;
this.log("receiveRegister(" + register + ") = " + value);
} else {
this.err("Received register out of bound: " + register);
}
}
receiveHeartbeat(message: Message) {
this.lastHeartbeat = new Date().getTime();
//console.log("*HB* Received HB");
this.log("Received heartbeat at " + (new Date()));
}
receiveInfo(next: CommRecord) {
if (next.message[1] === Rec.DBInfo) {
this.receiveDBInfo(next.message)
} else if (next.message[1] === Rec.NodeInfo) {
this.receiveNodeInfo(next.message);
} else if (next.message[1] === Rec.UnitInfo) {
this.receiveUnitInfo(next.message);
} else {
this.err("What is this? info type = " + next.message[1] + ", message: " + next.message);
}
this.system.triggerRebuild();
}
receiveSchedule(message: Message) {
this.schedule = message[2];
this.info("received week schedule = " + this.schedule);
}
receiveDateTime(message: Message) {
// 71,0,9,37,3,3,4,3,21,20 -> 09:37:03 Wednesday(3) 4 march 2120
this.date = new Date( (message[8]-1) * 100 + message[9], message[7]-1, message[6],
message[2], message[3], message[4]);
//this.info("Received date/time: " + this.date);
}
receiveDBInfo(message: Message) {
const dbInfo = Protocol.makeDBInfo(message);
this.nrNodes = dbInfo.nrNodes;
for (let nodeInx=0; nodeInx < this.nrNodes; nodeInx++) {
this.fetchNode(nodeInx);
}
}
receiveNodeInfo(message: Message) {
const nodeInfo = Protocol.makeNodeInfo(message);
let node = this.findNode(nodeInfo.logicalAddress);
if (!node) {
node = new Node(this, nodeInfo);
this.nodes.push(node);
} else {
Sanitizers.nodeInfo(nodeInfo, node);
}
this.log("> received node info: " + nodeInfo.name + " ("+nodeInfo.logicalAddress+")");
this.system.setActiveState(node);
this.config.nodenames[node.logicalAddress] = node.name;
this.system.updateMasterConfig(this);
if (node.active && (node.nrUnits !== node.units.length)) {
this.fetchAllUnits(node);
} else {
this.info("Skipping node: " + node.getDescription());
}
}
receiveUnitInfo(message: Message) {
const unitInfo = Protocol.makeUnitInfo(message);
let unit = this.findUnit(unitInfo.logicalNodeAddress, unitInfo.logicalAddress);
this.info("> received unit info: " + unitInfo.name + " ("+unitInfo.logicalNodeAddress+","+unitInfo.logicalAddress+") for unit: " + unit?.name);
if (!unit) {
// find if in config
const cunit = this.system.config.cunits.find(u => ((u.logicalNodeAddress == unitInfo.logicalNodeAddress) && (u.logicalAddress == unitInfo.logicalAddress)));
if (cunit) unitInfo.extendedType = cunit.extendedType;
const node = this.findNode(unitInfo.logicalNodeAddress);
if (node) {
unit = new Unit(node, unitInfo, this.system.config.mood);
node.units.push(unit);
} else {
this.err("Node not found: " + unitInfo.logicalNodeAddress);
}
} else {
Sanitizers.unitInfo(unitInfo, unit);
}
this.system.setActiveState(unit);
}
//////////////////
// request info //
//////////////////
async fetchAllUnits(node: Node) {
for (let unitInx=0; unitInx < node.nrUnits; unitInx++) {
await this.fetchUnit(node, unitInx);
}
}
async fetchDbInfo() {
try {
await this.send(Protocol.buildDBInfo());
await this.send(Protocol.buildRequestSchedule());
} catch (err) {
this.err("dbInfo call failed -> " + err);
}
}
async fetchNode(nodeInx: number) {
const message = Protocol.buildNodeInfo(nodeInx);
try {
await this.send(message);
} catch (err) {
this.err("nodeInfo call failed -> " + err);
}
}
async fetchUnit(node: Node, unitInx: number) {
const message = Protocol.buildUnitInfo(node, unitInx);
try {
// unit with index "unitInx" in node "logicalAddress"
await this.send(message);
} catch (err) {
this.err("unitInfo call failed -> " + err);
}
}
async getDatabase(readDB: boolean = false) {
this.nodes = [];
const hasNames = this.system.config.cunits.filter(u => this.same(u.masterAddress, u.masterPort)).some(u => u.name);
if (readDB || !hasNames) {
this.log("Fetching info from master DB for: " + this.getAddress());
await this.fetchDbInfo();
// upon reception of the DB info,
// getNode info will be called,
// which in it's turn will trigger getUnitInfo through fetchAllUnits
} else {
// loop over all nodes/units in the config with a matching ip address
// fill: this.nrNodes
// call: kind of receive-Node/Unit-Info
this.log("Building info from config for: " + this.getAddress());
this.system.config.cunits
.filter(u => this.same(u.masterAddress, u.masterPort))
.forEach(u => {
let node = this.findNode(u.logicalNodeAddress);
if (!node) {
let name = this.config.nodenames[u.logicalNodeAddress] ||
((u.logicalNodeAddress == 255) ? "Virtual Node" :
("Node-" + ((u.logicalNodeAddress < 16) ? '0' : '') + u.logicalNodeAddress.toString(16)));
node = new Node(this, {logicalAddress: u.logicalNodeAddress, name});
this.nodes.push(node);
this.system.setActiveState(node);
this.info("new node: " + node.getName())
}
let unit = this.findUnit(u.logicalNodeAddress, u.logicalAddress);
if (!unit) {
unit = new Unit(node, u);
node.units.push(unit);
this.info("new unit: " + unit.getName() + " -> " + u.logicalAddress);
}
this.system.setActiveState(unit);
});
}
}
allNodes(doToNode:(n: Node) => void) {
this.nodes.forEach(node => {
doToNode(node)
});
}
allUnits(doToUnit:(u: Unit) => void) {
this.nodes.forEach(node => {
node.units.forEach(unit => {
doToUnit(unit)
});
});
}
displayDatabase(onlyNodes = false) {
this.info("Showing " + this.nodes.length + " nodes");
this.nodes.forEach((node, nodeInx) => {
if (onlyNodes) this.log("===================================================================================")
this.log(nodeInx + ". " + node.name +
", type = " + node.typeName() +
", nrUnits = " + node.nrUnits +
", logical address = " + node.logicalAddress);
if (onlyNodes) {
this.log("-----------------------------------------------------------------------------------")
node.units.forEach((unit, unitInx) => {
this.log("> " + unitInx + ". '" + unit.name + "' => '" + unit.getName() +
"', type = " + unit.typeName() +
", logical address: " + unit.logicalAddress +
", value: " + unit.value +
(unit.status ? (", status = " + unit.status) : ""));
});
}
});
}
findUnit(logicalNodeAddress: number, logicalAddress: number) {
const node = this.findNode(logicalNodeAddress);
if (node)
return node.units.find((u: Unit) => u.logicalAddress === logicalAddress);
else
return null;
}
findNode(logicalAddress: number) {
return this.nodes.find((n: Node) => n && (n.logicalAddress === logicalAddress));
}
getNodeAndUnit(node: number | Node, unit: number | Unit) {
// allow for index numbers or real nodes to be passed
if (typeof node === "number") {
if (node >= this.nodes.length) {
this.err("getNodeAndUnit -> node nr " + node + " out of bounds");
node = null;
} else {
node = this.nodes[node];
}
}
if (! node) {
this.err("getNodeAndUnit -> node not found ");
return null;
}
if (typeof unit === "number") {
if (unit >= (<Node>node).units.length) {
this.err("getNodeAndUnit -> unit nr " + unit + " out of bounds");
unit = null;
} else {
unit = (<Node>node).units[unit];
}
}
if (! unit) {
this.err("getNodeAndUnit -> unit not found");
return null;
}
return { node, unit };
}
/* ****************************************************************************** */
/* Status requests */
/* Database (all known nodes) */
/* Node (all known units in this node) */
/* Unit (only dimmer, switch, input, temperature, motor and mood implemented) */
/* ****************************************************************************** */
receiveStatus(next: CommRecord) {
// called when an incoming message was received and classified as a status message
// find node
const nodeLogical = next.message[2];
const node = this.nodes.find(node => node && (node.logicalAddress == nodeLogical));
if (! node) {
this.info("status message " + next.cmd + " for unknown node = " + nodeLogical);
return;
}
// find unit
const unitLogical = next.message[3];
const unit = node.units.find(unit => unit && (unit.logicalAddress == unitLogical))
if (!unit) {
this.info("status message " + next.cmd + " for unknown unit = " + unitLogical +
" in node = " + nodeLogical);
return;
}
// Parse the status into the unit and propagate
Protocol.receiveStatus(next, unit);
}
async requestStatus() {
for (let nodeInx = 0; nodeInx < this.nodes.length; nodeInx++) {
const node = this.nodes[nodeInx];
if (node.active) {
for (let unitInx = 0; unitInx < node.units.length; unitInx++) {
await this.requestUnitStatus(node.units[unitInx]);
}
}
}
}
async requestNodeStatus(node: Node) {
for (let unitInx = 0; unitInx < node.units.length; unitInx++) {
if (node.units[unitInx].active)
await this.requestUnitStatus(node.units[unitInx]);
}
}
async requestUnitStatus(unit: Unit) {
const message = Protocol.buildRequestUnitStatus(unit.node, unit)
let res = await this.send(message);
// results will be set by the data event listener
this.info("get value of " + unit.node.getName() + "-" + unit.getName());
}
async requestRegisterValue(register: number) {
if (this.protocol >= 209) {
await this.send(Protocol.buildRegister(register));
this.info("request register: " + register);
} else {
this.err("FC_REGISTERMAP not available before protocol v2.09")
}
}
/////////////////////
// Settings values //
/////////////////////
async setUnitStatus(unit: Unit, value: number) {
const params = Protocol.buildSetCmd(unit.node, unit, value);
if (params.cmd) {
await this.send(params.message);
}
}
async setRegisterValue(register: number, value: number) {
if (this.protocol >= 209) {
await this.send(Protocol.buildRegister(register, value));
this.info("set register: " + register + ", to: " + value);
} else {
this.err("FC_REGISTERMAP not available before protocol v2.09")
}
}
async setSensorValue(unit: Unit, value: number) {
this.info("SetSensorValue of " + unit.node.logicalAddress + "/" + unit.logicalAddress + " to " + value);
await this.send(Protocol.buildSetPreset(unit.node, unit, TempPreset.sun, Math.trunc(value / 100 / 100 / 100) % 100));
await this.send(Protocol.buildSetPreset(unit.node, unit, TempPreset.hsun, Math.trunc(value / 100 / 100) % 100));
await this.send(Protocol.buildSetPreset(unit.node, unit, TempPreset.moon, Math.trunc(value / 100) % 100));
await this.send(Protocol.buildSetPreset(unit.node, unit, TempPreset.hmoon, Math.trunc(value % 100)));
}
async setSensorStatus(unit: Unit, value: number) {
await this.send(Protocol.buildSetSensor(unit.node, unit, value));
}
async setPreset(unit: Unit, preset: number, temp: number) {
await this.send(Protocol.buildSetPreset(unit.node, unit, preset, temp));
this.info("set temp preset: " + preset + " of " + unit.node.getName() + "-" + unit.getName() + " to temp " + temp);
}
async selectPreset(unit: Unit, preset: number) {
await this.send(Protocol.buildSelectPreset(unit.node, unit, preset));
this.info("set temp preset of " + unit.node.getName() + "-" + unit.getName() + " to: " + preset);
}
async setSchedule() {
await this.send(Protocol.buildSendSchedule(this.schedule));
this.info("set schedule to week nr: " + this.schedule);
}
async setTempOnOff(unit: Unit, on: boolean) {
await this.send(Protocol.buildSensorOnOff(unit.node, unit, on));
this.info("turn temp sensor of " + unit.node.getName() + "-" + unit.getName() + ": " + (on ? "on" : "off"));
}
async doIncDecPreset(unit: Unit, inc: boolean) {
await this.send(Protocol.buildIncDecPreset(unit.node, unit, inc));
this.info("set temp preset of " + unit.node.getName() + "-" + unit.getName() + ": " + (inc ? "up" : "down"));
}
}