UNPKG

homebridge-smartsystem

Version:

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

310 lines (260 loc) 9.56 kB
import * as mqtt from "mqtt"; import { MqttClient } from "mqtt"; import { System } from "../duotecno/system"; import { SmappeeConfig, SwitchType } from "../duotecno/types"; import { debug, err, log } from "../duotecno/logger"; import { PowerBase } from "./powerbase"; import { Context } from "./webapp"; // Smappee MQTT implementation // Johan Coppieters, Jan 2019. // v2: rules can now add multiple channels, Jan 2021 // // Testing Raspberry: // mqtt sub -t '#' -h '192.168.99.75' -v => uid // mqtt sub -t 'servicelocation/57e3e0d8-bb05-4b04-8662-1a9871998f3f/#' -h '192.168.99.54' -v // // Testing Mac: // /Users/johan/.nvm/versions/node/v10.16.0/bin/mqtt sub -t 'servicelocation/57e3e0d8-bb05-4b04-8662-1a9871998f3f/#' -h '192.168.99.54' -v // // see smappee.json for output export class Smappee extends PowerBase { config: SmappeeConfig; // redefine client: MqttClient; plugs: { [nodeId: number]: { value: boolean, name: string } }; constructor(system: System) { super("smappee", system); this.client = null; // init is called by super } // // start and restart services // async init(firstTime: boolean) { // will grow when we encounter one in the mqtt stream or if we receive a config message this.plugs = {}; return super.init(firstTime); } // // return data for ejs pages // async getData(context: Context, message: String): Promise<any> { const data = await super.getData(context, message); // add plugs to context data["plugs"] = this.plugs; return data; } // // config mgt // async updateConfig(config: any) { this.config.uid = config.uid; return super.updateConfig(config); } // // subscribe to mqtt stream // async subscribe(): Promise<void> { if (this.config.address && this.config.uid) return new Promise((resolve, reject) => { try { const client = mqtt.connect('mqtt:' + this.config.address); this.client = client; log("smappee", "connecting to " + 'mqtt:' + this.config.address); client.on('connect', () => { client.subscribe('servicelocation/' + this.config.uid + '/#', (error) => { if (error) { err("smappee", error.message); reject(); } else { log("smappee", "subscribed to " + 'servicelocation/' + this.config.uid + "/# -- all messages"); resolve(); } }); }); client.on('message', (topic, buffer) => { // example topics: // servicelocation/57e3e0d8-bb05-4b04-8662-1a9871998f3f/plug/1/state // servicelocation/57e3e0d8-bb05-4b04-8662-1a9871998f3f/realtime try { const message = JSON.parse(buffer.toString()); const parts = topic.split("/"); if (parts.length > 2) { if (this.isRealTime(parts)) // && (message.utcTimeStamp % 5000 === 0)) this.processRealTime(message); else if (this.isPlug(parts)) this.processPlug(this.getPlugNr(parts), message); else if (this.isHomeControl(parts)) this.processHomeControl(message); else if (this.isChannelConfig(parts)) this.processChannelConfig(message); } } catch(error) { err("smappee", "Error converting incoming : " + topic + " -> " + JSON.stringify(error) + " -> " + buffer.toString()); } }); } catch(err) { err("smappee", err); reject(); } }); } // // Alternative in above, more specific subscribes, needs also separate on-message-handlers // + don't forget also the home-control and channel-config messages. // // client.subscribe('servicelocation/' + uid + '/realtime', (error) => { // if (error) { // err("smappee", error.message); // } else { // log("smappee", "subscribed to " + 'servicelocation/' + uid + "/realtime"); // } // }); // client.subscribe('servicelocation/' + uid + '/plug/#', (error) => { // if (error) { // err("smappee", error.message); // } else { // log("smappee", "subscribed to " + 'servicelocation/' + uid + "/plug/#"); // } // }); async unsubscribe(): Promise<void> { if (this.client) { return new Promise<void>((resolve, reject) => { try { this.client.end(true, () => { log("smappee", "unsubscribed from the MQTT stream"); resolve(); }); } catch(err) { err("smappee", err); reject(); } this.client = null; }); } } isRealTime(parts) { return (parts.length >= 3) && (parts[2] === "realtime"); } isPlug(parts) { return (parts.length >= 3) && (parts[2] === "plug"); } isHomeControl(parts) { return (parts.length >= 3) && (parts[2] === "homeControlConfig"); } isChannelConfig(parts) { return (parts.length >= 3) && (parts[2] === "channelConfigV2"); } getPlugNr(parts): number { return (parts.length >= 3) ? parseInt(parts[3]) : 0; } processChannelConfig(message) { // console.log("ChannelConfig", message.dataProcessingSpecification.measurements); this.channels = {}; message.dataProcessingSpecification.measurements.forEach(m => { const inx = m.publishIndex; if (inx >= 0) { if (!this.channels[inx]) this.channels[inx] = <any>{}; this.channels[inx].name = m.name; this.channels[inx].type = m.type; this.channels[inx].flow = m.flow; } }); } processHomeControl(message) { message.switchActuators.forEach(s => { const id = parseInt(s.nodeId); if (! this.switches[id]) this.switches[id] = {value: false, name: s.name}; else this.switches[id].name = s.name; }); message.smartplugActuators.forEach(s => { const id = parseInt(s.nodeId); if (! this.plugs[id]) this.plugs[id] = {value: false, name: s.name}; else this.plugs[id].name = s.name; }); } processRealTime(message) { this.realtimeCounter++; // update every 5 messages if (this.realtimeCounter % 5 === 0) { this.realtime = { totalPower: message.totalPower, totalReactivePower: message.totalReactivePower, totalExportEnergy: message.totalExportEnergy, totalImportEnergy: message.totalImportEnergy, monitorStatus: message.monitorStatus, utcTimeStamp: message.utcTimeStamp }; message.voltages.forEach(v => { this.voltages[v.phaseId] = v.voltage; }); message.channelPowers.forEach(p => { const inx = p.publishIndex; if (! this.channels[inx]) { // if we didn't receive a config message, give fake names and type. this.channels[inx] = <any> {name: "CH-"+inx, flow: "-", type: "-"}; } this.channels[inx].power = p.power; this.channels[inx].exportEnergy = p.exportEnergy; this.channels[inx].importEnergy = p.importEnergy; this.channels[inx].phaseId = p.phaseId; this.channels[inx].voltage = this.voltages[p.phaseId] || 0; this.channels[inx].current = p.current; this.channels[inx].apparentPower = p.apparentPower; this.channels[inx].cosPhi = p.cosPhi; this.channels[inx].formula = p.formula; }); this.calcProducAndConsumption(message); } // check rules this.applyRules(); // check bindings this.applyBindings(); } calcProducAndConsumption(message) { this.production = 0; this.consumption = 0; message.channelPowers.forEach(p => { const channel = this.channels[p.publishIndex]; if (channel) { if (channel.flow == "PRODUCTION") { this.production += channel.power; } else { this.consumption += channel.power; } } }); // consumption seems to be wrong... ?? this.consumption = this.realtime.totalPower; } /////////// // Plugs // /////////// emptyPlug(nr: number) { return {value: null, name: "New plug - "+nr}; } processPlug(plugNr: number, message) { const newState = (message.value == "ON"); if (!this.plugs[plugNr]) this.plugs[plugNr] = this.emptyPlug(plugNr); if (this.plugs[plugNr].value != newState) { debug("smappee", "doPlug, plugNr = " + plugNr + ", received: " + message.value + ", current: " + this.plugs[plugNr].value); this.plugs[plugNr].value = newState; // send status change to system this.system.emitter.emit('plug', SwitchType.kSmappee, plugNr, newState); } } setPlug(plugNr: number, state: boolean) { if (!this.plugs[plugNr]) this.plugs[plugNr] = this.emptyPlug(plugNr); const currState = this.plugs[plugNr].value; if ((typeof currState === "boolean") && (state != currState)) { // do we need to do this? do we get an mqtt status message ?? this.plugs[plugNr].value = currState; if (this.client) { const topic = 'servicelocation/' + this.config.uid + '/plug/' + plugNr + '/setstate'; const payload = '{"value": "' + ((state) ? "ON" : "OFF") + '", "since": "' + (new Date().getTime()) + '"}'; this.client.publish(topic, payload); } else { err("smappee", "no open connection (yet?) to " + this.config.uid + " on " + this.config.address); } } } }