homebridge-smartsystem
Version:
SmartServer (Proxy Websockets to TCP sockets, Smappee MQTT, Duotecno IP Nodes, Homekit interface)
295 lines • 12.6 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Smappee = void 0;
const mqtt = require("mqtt");
const types_1 = require("../duotecno/types");
const logger_1 = require("../duotecno/logger");
const powerbase_1 = require("./powerbase");
// 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
class Smappee extends powerbase_1.PowerBase {
constructor(system) {
super("smappee", system);
this.client = null;
// init is called by super
}
//
// start and restart services
//
init(firstTime) {
const _super = Object.create(null, {
init: { get: () => super.init }
});
return __awaiter(this, void 0, void 0, function* () {
// will grow when we encounter one in the mqtt stream or if we receive a config message
this.plugs = {};
return _super.init.call(this, firstTime);
});
}
//
// return data for ejs pages
//
getData(context, message) {
const _super = Object.create(null, {
getData: { get: () => super.getData }
});
return __awaiter(this, void 0, void 0, function* () {
const data = yield _super.getData.call(this, context, message);
// add plugs to context
data["plugs"] = this.plugs;
return data;
});
}
//
// config mgt
//
updateConfig(config) {
const _super = Object.create(null, {
updateConfig: { get: () => super.updateConfig }
});
return __awaiter(this, void 0, void 0, function* () {
this.config.uid = config.uid;
return _super.updateConfig.call(this, config);
});
}
//
// subscribe to mqtt stream
//
subscribe() {
return __awaiter(this, void 0, void 0, function* () {
if (this.config.address && this.config.uid)
return new Promise((resolve, reject) => {
try {
const client = mqtt.connect('mqtt:' + this.config.address);
this.client = client;
(0, logger_1.log)("smappee", "connecting to " + 'mqtt:' + this.config.address);
client.on('connect', () => {
client.subscribe('servicelocation/' + this.config.uid + '/#', (error) => {
if (error) {
(0, logger_1.err)("smappee", error.message);
reject();
}
else {
(0, logger_1.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) {
(0, logger_1.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/#");
// }
// });
unsubscribe() {
return __awaiter(this, void 0, void 0, function* () {
if (this.client) {
return new Promise((resolve, reject) => {
try {
this.client.end(true, () => {
(0, logger_1.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) {
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] = {};
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] = { 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) {
return { value: null, name: "New plug - " + nr };
}
processPlug(plugNr, message) {
const newState = (message.value == "ON");
if (!this.plugs[plugNr])
this.plugs[plugNr] = this.emptyPlug(plugNr);
if (this.plugs[plugNr].value != newState) {
(0, logger_1.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', types_1.SwitchType.kSmappee, plugNr, newState);
}
}
setPlug(plugNr, state) {
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 {
(0, logger_1.err)("smappee", "no open connection (yet?) to " + this.config.uid + " on " + this.config.address);
}
}
}
}
exports.Smappee = Smappee;
//# sourceMappingURL=smappee.js.map