homebridge-smartsystem
Version:
SmartServer (Proxy TCP sockets to the cloud, Smappee MQTT, Duotecno IP Nodes, Homekit interface)
310 lines (260 loc) • 9.56 kB
text/typescript
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);
}
}
}
}