homebridge-smartsystem
Version:
SmartServer (Proxy TCP sockets to the cloud, Smappee MQTT, Duotecno IP Nodes, Homekit interface)
1,053 lines • 56.4 kB
JavaScript
"use strict";
// SmartApp implementation with Webapp
// Purpose:
// - select IP nodes & units to include -> generate config file
// - attach Smappee and rules -> update config file
// - control units from the IP nodes as test
//
// Johan Coppieters, Feb 2019.
//
// v2.0: mar/apr 2020
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.SmartApp = void 0;
const webapp_1 = require("./webapp");
const types_1 = require("../duotecno/types");
const logger_1 = require("../duotecno/logger");
const protocol_1 = require("../duotecno/protocol");
const somfy = require("./somfy");
const fs_1 = require("fs");
const config_1 = require("../duotecno/config");
const HB_1 = require("./HB");
const HA_API_1 = require("./HA-API");
const proxy_1 = require("./proxy");
const kMaster = { name: "master", type: "string", default: "0.0.0.0:5001" };
const kAddress = { name: "address", type: "string", default: "0.0.0.0" };
const kAddresses = { name: "addresses", type: "string", default: "[0.0.0.0]" };
const kAddressOld = { name: "addressX", type: "string", default: "0.0.0.0" };
const kPort = { name: "port", type: "integer", default: 80 };
const kPortOld = { name: "portX", type: "integer", default: 80 };
const kActive = { name: "active", type: "string", default: "N" };
const kUID = { name: "uid", type: "string", default: "" };
const kName = { name: "name", type: "string", default: "no name" };
const kPassword = { name: "password", type: "string", default: "no password" };
const kNode = { name: "node", type: "integer", default: 0 };
const kUnit = { name: "unit", type: "integer", default: 0 };
const kValue = { name: "value" };
const kIntValue = { name: "value", type: "integer", default: 0 };
const kPin = { name: "pin", type: "string", default: "577-03-001" };
// Mac file system -> write locally
const isMac = (0, fs_1.existsSync)("/Volumes");
const kDHCPConfigFile = isMac ? "./config.dhcpcd" : "/etc/dhcpcd.conf";
class SmartApp extends webapp_1.WebApp {
constructor(system, power, platform) {
super("smartapp");
this.system = system;
// get status change updates
this.system.emitter.on('update', this.informChange.bind(this));
this.system.emitter.on('update', this.informLinks.bind(this));
this.system.emitter.on('plug', this.alertSwitch.bind(this));
// get some configurated params
this.port = this.config.port || this.port || 80;
this.password = platform.config.password || this.config.password || "";
this.user = platform.config.user || this.config.user || "pi";
this.system = system;
this.power = power;
this.platform = platform;
// when all masters are loaded -> attach units to the switches
this.system.emitter.on('ready', this.initSwitchUnits.bind(this));
this.system.emitter.on('ready', this.initLinks.bind(this));
this.addFile("unitList", __dirname + "/views/unit-list.ejs", "application/json");
this.addFile("masterList", __dirname + "/views/master-list.ejs", "text/html");
this.addFile("masterDetail", __dirname + "/views/master-detail.ejs", "text/html");
this.addFile("nodeDetail", __dirname + "/views/node-details.ejs", "text/html");
this.addFile("serviceList", __dirname + "/views/service-list.ejs", "text/html");
this.addFile("power", __dirname + "/views/power.ejs", "text/html");
this.addFile("login", __dirname + "/views/login.ejs", "text/html");
this.addFile("settings", __dirname + "/views/settings.ejs", "text/html");
this.addFile("switchDetail", __dirname + "/views/switch-details.ejs", "text/html");
this.addFile("switchList", __dirname + "/views/switch-list.ejs", "text/html");
this.addFile("linkDetail", __dirname + "/views/link-details.ejs", "text/html");
this.addFile("linkList", __dirname + "/views/link-list.ejs", "text/html");
this.addFile("powerRule", __dirname + "/views/power-rule.ejs", "text/html");
this.addFile("powerBinding", __dirname + "/views/power-binding.ejs", "text/html");
this.addFile("materializeCSS", __dirname + "/views/assets/materialize.min.css", "text/css");
this.addFile("materializeJS", __dirname + "/views/assets/materialize.min.js", "text/javascript");
this.addFile("favicon", __dirname + "/views/assets/favicon.ico", "image/x-icon");
this.addFile("proxy", __dirname + "/views/proxy.ejs", "text/html");
}
writeConfig() {
// copy switches into config, eliminate the runtime stuff (like unit)
this.config.switches = this.switches.map(s => types_1.Sanitizers.makeSwitchConfig(s));
this.config.links = this.links.map(s => types_1.Sanitizers.makeLinkConfig(s));
super.writeConfig();
}
readConfig() {
super.readConfig();
// copy switches from config
this.switches = this.config.switches.map(s => types_1.Sanitizers.switchConfig(s));
this.links = this.config.links.map(s => types_1.Sanitizers.linkConfig(s));
}
checkReady(context) {
if (this.platform && !this.platform.ready) {
context["notReady"] = true;
context["notReadyMessage"] = "=== waiting >> found " +
this.system.allActiveUnits().length + " units out of " + this.system.activeUnitsConfig().length + " selected after " +
this.platform.startWaiting + " sec ===";
(0, logger_1.log)("smartapp", "Platform not ready -> " + context["notReadyMessage"]);
}
else {
context["notReady"] = false;
context["notReadyMessage"] = "";
}
}
//////////////
// password //
//////////////
pwOK(context, user, pw) {
return __awaiter(this, void 0, void 0, function* () {
// to bypass user/pw ->
//return true;
this.token = "";
(0, config_1.setToken)("");
try {
const access_token = yield this.checkLogin(this.config.apiHost + ":" + this.config.apiPort, user, pw);
if (access_token) {
// save username / password in config
this.config.username = user;
this.config.password = pw;
this.writeConfig();
this.token = access_token;
(0, config_1.setToken)(this.token);
return true;
}
}
catch (e) {
(0, logger_1.debug)("smartapp", "Login API failed " + JSON.stringify(e));
}
return false;
});
}
//////////////////////////////
// Router //
//////////////////////////////
needsLogin(context) {
const noNeed = ((context.request === "units") || (context.request === "switches") || (context.request === "links")) &&
((context.action == "press") || (context.action == "get") || (context.action == "set"));
return super.needsLogin(context) && (context.request != "files") && (!noNeed);
}
doRequest(context) {
const _super = Object.create(null, {
doRequest: { get: () => super.doRequest }
});
return __awaiter(this, void 0, void 0, function* () {
this.checkReady(context);
if (context.request === "")
context.request = "masters";
context["hasSmappee"] = !!this.power.smappee;
context["hasP1"] = !!this.power.p1;
context["hasShelly"] = !!this.power.shelly;
// try "[get]/set" url: node/unit[/status]
const res = yield this.tryNumURL(context);
if (res) {
return res;
}
else if (context.request === "files") {
return this.renderAssets(context);
}
else if (context.request === "images") {
return this.renderImage(context);
}
else if (context.request === "masters") {
return this.doMasters(context);
}
else if (context.request === "units") {
return this.doUnits(context);
}
else if (context.request === "services") {
return this.doServices(context);
}
else if (context.request === "power") {
return this.doPower(context);
}
else if (context.request === "links") {
return this.doLinks(context);
}
else if (context.request === "switches") {
return this.doSwitches(context);
}
else if (context.request === "settings") {
return this.doSettings(context);
}
else if (context.request === "proxy") {
return this.doProxy(context);
}
else {
return _super.doRequest.call(this, context);
}
});
}
tryNumURL(context) {
return __awaiter(this, void 0, void 0, function* () {
const node = context.nums[0];
const master = this.system.masters[0];
if ((!node) || (!master))
return null;
const unit = context.nums[1];
const state = context.nums[2];
if (typeof state === "undefined")
return this.json(yield this.getState(master, node, unit));
else
return this.json(yield this.setState(master, node, unit, state));
});
}
scrapeUnit(context, boundary) {
context.getMaster("action");
const master = this.system.findMaster(context["masterAddress"], context["masterPort"]);
let unit = null;
let logicalNodeAddress, logicalAddress;
let name = context.getParam({ name: "unit" + boundary, type: "string", default: "--" });
const value = (0, types_1.actionValue)(context.getParam({ name: "value" + boundary, type: "string", default: "0" }));
// hex addresses or name
if ((name[0] === "0") && (name[1] === "x")) {
({ logicalNodeAddress, logicalAddress } = context.addr(name));
unit = this.system.findUnit(master, logicalNodeAddress, logicalAddress);
name = (unit) ? unit.displayName : "--";
}
else {
unit = this.system.findUnitByName(master, name);
logicalNodeAddress = (unit) ? unit.node.logicalAddress : 0;
logicalAddress = (unit) ? unit.logicalAddress : 0;
}
return { name, value, masterAddress: context["masterAddress"], masterPort: context["masterPort"],
logicalAddress, logicalNodeAddress };
}
///////////
// Proxy //
///////////
doProxy(context) {
return __awaiter(this, void 0, void 0, function* () {
let config = this.config.proxy || proxy_1.kEmptyProxy;
if (context.action === "save") {
// save the proxy config
config.cloudServer = context.getParam({ name: "cloudServer", type: "string" });
config.cloudPort = context.getParam({ name: "cloudPort", type: "integer" });
config.masterAddress = context.getParam({ name: "masterAddress", type: "string" });
config.masterPort = context.getParam({ name: "masterPort", type: "integer" });
config.uniqueId = context.getParam({ name: "uniqueId", type: "string" });
this.write("proxy", config);
(0, proxy_1.setProxyConfig)(config);
}
else if (context.action === "restart") {
(0, proxy_1.cleanStart)(true);
}
else {
// just show the proxy config
}
return this.ejs("proxy", context, { config });
});
}
//////////////
// Settings //
//////////////
doSettings(context) {
return __awaiter(this, void 0, void 0, function* () {
let config = this.read("settings");
if (context.action === "save") {
config = this.scrapeSettings(context);
this.write("settings", config);
}
else if (context.action === "restart") {
context.request = "restart";
return this.doRestart(false);
}
else if (context.action === "install") {
config = this.scrapeSettings(context);
context.request = "install";
this.write("settings", config);
this.writeDHCP(context, config);
}
else if (context.action === "reset") {
config = Object.assign({}, types_1.kEmptySettings);
this.resetDHCP(context);
this.write("settings", config);
return this.doReboot(context, false);
}
else if (context.action === "reboot") {
return this.doReboot(context, false);
}
else {
}
return this.ejs("settings", context, { config });
});
}
scrapeSettings(context) {
return types_1.Sanitizers.settings({
network: {
ip1: context.getParam({ name: "ip1", type: "string" }),
gateway1: context.getParam({ name: "gateway1", type: "string" }),
nameservers: context.getParam({ name: "nameservers", type: "string" }),
ip2: context.getParam({ name: "ip2", type: "string" }),
gateway2: context.getParam({ name: "gateway2", type: "string" })
}
});
}
writeDHCP(context, config) {
try {
this.resetDHCP(context);
(0, fs_1.writeFileSync)(kDHCPConfigFile, this.DHCPdFile(config.network));
}
catch (err) {
context.message = JSON.stringify(err);
}
}
resetDHCP(context) {
try {
if ((0, fs_1.existsSync)(kDHCPConfigFile))
(0, fs_1.truncateSync)(kDHCPConfigFile);
}
catch (err) {
context.message = JSON.stringify(err);
}
}
DHCPdFile(config) {
const ip1 = "interface eth0\n" +
"static ip_address=" + config.ip1 + "\n" +
"static routers=" + config.gateway1 + "\n\n" +
"static domain_name_servers=" + config.nameservers;
if (config.ip2) {
return ip1 + "\n\n" +
"interface eth0:0\n" +
"static ip_address=" + config.ip2 + "\n" +
"static routers=" + config.gateway2;
}
else {
return ip1;
}
}
//////////////////////////////
// Power Stuff //
//////////////////////////////
doPower(context) {
return __awaiter(this, void 0, void 0, function* () {
let message;
const id = parseInt(context.id);
const type = context.getParam({ name: "type", type: "string", default: "smappee" });
const power = this.power[type];
if (!power) {
return this.error(context, "Power type not implemented", false);
}
// remember the power-type
context["type"] = type;
// if not found or action done -> drop through and list again the power attributes + all rules
try {
if (context.action === "add") {
const rule = power.newRule();
return this.ejs("powerRule", context, { rule, id: power.rules.length - 1, masters: this.system.masters, type });
}
else if (context.action === "rule") {
if (id >= 0)
return this.ejs("powerRule", context, { rule: power.rules[id], id, masters: this.system.masters, type });
}
else if (context.action === "delete") {
if (id >= 0)
power.deleteRule(id);
}
else if (context.action === "change") {
power.updateRule(id, this.scrapeRule(context));
}
else if (context.action === "save") {
power.updateConfig({ address: context.getParam(kAddress), uid: context.getParam(kUID), addresses: context.getParam(kAddresses) });
}
else if (context.action === "badd") {
const binding = power.newBinding();
return this.ejs("powerBinding", context, { binding, id: power.bindings.length - 1, masters: this.system.masters, type });
}
else if (context.action === "binding") {
if (id >= 0)
return this.ejs("powerBinding", context, { binding: power.bindings[id], id, masters: this.system.masters, type });
}
else if (context.action === "bdelete") {
if (id >= 0)
power.deleteBinding(id);
}
else if (context.action === "bchange") {
power.updateBinding(id, this.scrapeBinding(context));
}
else {
return this.ejs("power", context, yield power.getData(context, ""));
}
}
catch (e) {
message = e.toString();
}
return this.ejs("power", context, yield power.getData(context, ""));
});
}
scrapeBinding(context) {
context.getMaster("action");
const register = context.getParam({ name: "register", type: "integer", default: 0 });
const channel = context.getParam({ name: "channel", type: "string", default: "0" });
return { masterAddress: context["masterAddress"], masterPort: context["masterPort"],
register, channel };
}
scrapeAction(context, boundary) {
const { name, value, masterAddress, masterPort, logicalAddress, logicalNodeAddress } = this.scrapeUnit(context, boundary);
return {
name, value, masterAddress, masterPort, logicalAddress, logicalNodeAddress
};
}
scrapeRule(context) {
// deep copy an empty rule
let rule = Object.assign({}, types_1.kEmptyRule); // = shallow copy, re-assign actions later
// get the form values
rule.type = context.getParam({ name: "rtype", type: "string", default: rule.type });
rule.channel = context.getParam({ name: "channel", type: "string", default: rule.channel });
rule.low = context.getParam({ name: "low", type: "integer", default: rule.low });
rule.high = context.getParam({ name: "high", type: "integer", default: rule.high });
rule.actions = [this.scrapeUnit(context, "low"),
this.scrapeUnit(context, "mid"),
this.scrapeUnit(context, "high")];
return rule;
}
///////////
// Links //
///////////
initLinks() {
(0, logger_1.log)("smartapp", "Init " + this.links.length + " Links -> add units");
this.links.forEach(link => {
link.unit || (link.unit = this.system.findUnit(this.system.findMaster(link.masterAddress, link.masterPort), link.logicalNodeAddress, link.logicalAddress));
if (link.unit) {
link.dtValue = link.unit.value;
}
else {
(0, logger_1.log)("smartapp", "** error ** missing unit: " + (0, types_1.hex)(link.logicalNodeAddress) + "/" + (0, types_1.hex)(link.logicalAddress) + " **");
}
});
(0, HB_1.hbSubscribe)(() => __awaiter(this, void 0, void 0, function* () {
// Get all values for subscribed accessories (links)
if (this.links.length && (yield this.checkToken())) {
for (const link of this.links) {
if ((link.type === types_1.LinkType.kAccessory) && link.unit) {
const hbValue = yield (0, HB_1.hbGetValue)(this.config.apiHost + ":" + this.config.apiPort, link.accId, link.accService, this.token);
(0, logger_1.debug)("smartapp", "Received HB value for accessory: " + link.accName + " -> " + hbValue + " from accessory");
if (typeof hbValue != "undefined") {
const { dtValue, dtTarget } = (0, HB_1.hb2dtvalue)(link.unit, hbValue);
if (dtValue !== link.dtValue) { // || state !== link.status
(0, logger_1.log)("smartapp", "-> Setting value of Duotecno unit: " + link.unit.getDisplayName() + " to " + dtTarget + " of HB accessory: " + link.accName + " with value " + hbValue);
link.dtValue = dtValue;
link.unit.setState(dtTarget);
}
else {
(0, logger_1.debug)("smartapp", "-> No change for unit: " + link.unit.getDisplayName() + " to " + dtValue);
}
}
}
}
}
}));
}
informLinks(u, kind) {
return __awaiter(this, void 0, void 0, function* () {
(0, logger_1.debug)("smartapp", "Received DT value from unit: " + u.getDisplayName() + " -> " + u.value + " / " + u.status + " from unit");
for (const link of this.links) {
if (u.isUnit(link.masterAddress, link.masterPort, link.logicalNodeAddress, link.logicalAddress)) {
const hbValue = (0, HB_1.dt2hbvalue)(u, link.hbValue, link.min, link.max);
if (typeof hbValue != "undefined") {
if (hbValue === -1) {
(0, logger_1.log)("smartapp", "-> Virtual unit send 'stop' for accessory: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
const val = yield (0, HB_1.hbStop)(this.config.apiHost + ":" + this.config.apiPort, link.accId, link.accService, this.token);
if (typeof val != "undefined")
link.hbValue = val;
}
else if (hbValue !== link.hbValue) {
(0, logger_1.log)("smartapp", "-> Setting HB value for accessory: " + link.accName + " to " + hbValue + " of Duotecno unit: " + u.getDisplayName() + " with value " + u.value);
link.hbValue = hbValue;
yield (0, HB_1.hbSetState)(this.config.apiHost + ":" + this.config.apiPort, link.accId, link.accService, hbValue, this.token);
}
else {
(0, logger_1.debug)("smartapp", "-> No change for accessory: " + link.accName + " to " + u.value + " / " + u.status);
}
}
}
}
;
});
}
checkToken() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.token) {
const access_token = yield (0, HB_1.hbLogin)(this.config.apiHost + ":" + this.config.apiPort, this.config.username, this.config.password);
if (!access_token) {
(0, logger_1.err)("smartapp", "checkToken: failed to get token");
return false;
}
this.token = access_token;
(0, config_1.setToken)(this.token);
}
return true;
});
}
getAccessories() {
return __awaiter(this, void 0, void 0, function* () {
if (yield this.checkToken())
return (0, HB_1.hbAccessories)(this.config.apiHost + ":" + this.config.apiPort, this.token);
else
return [];
});
}
doLinks(context) {
return __awaiter(this, void 0, void 0, function* () {
let inx = parseInt(context.id);
const accessories = yield this.getAccessories();
const units = this.system.allUsedUnits();
let message;
try {
if (context.action === "add") {
this.links.push(Object.assign(Object.assign({}, types_1.kEmptyLink), { type: types_1.LinkType.kAccessory }));
inx = this.links.length - 1;
return this.ejs("linkDetail", context, { link: this.links[inx], id: inx,
masters: this.system.masters, accessories, units });
}
else if (context.action === "edit") {
return this.ejs("linkDetail", context, { link: this.links[inx], id: inx,
masters: this.system.masters, accessories, units });
}
else if (context.action === "delete") {
this.deleteLink(inx);
}
else if (context.action === "change") {
this.updateLink(inx, this.scrapeLink(context));
}
else {
// possible new IP Nodes, hence Units could be online
this.initLinks();
}
}
catch (e) {
message = e.toString();
}
return this.ejs("linkList", context, { masters: this.system.masters, links: this.links, message });
});
}
scrapeLink(context) {
// don't take the name of the unit
const { masterAddress, masterPort, logicalAddress, logicalNodeAddress } = this.scrapeUnit(context, '');
// for now only accessory links.
const stype = context.getParam({ name: "type", type: "string", default: types_1.LinkType.kAccessory });
const name = context.getParam({ name: "name", type: "string", default: "--" });
// max & min values
const min = context.getParam({ name: "min", type: "integer", default: 0 });
const max = context.getParam({ name: "max", type: "integer", default: 100 });
// uniqiue id for the accessory
const accId = context.getParam({ name: "accId", type: "string", default: "" });
const accName = (0, HB_1.hbName)(accId);
const accService = (0, HB_1.hbService)(accId);
return { accId, accName, accService, name, masterAddress, masterPort, logicalAddress, logicalNodeAddress, type: stype, min, max };
;
}
updateLink(inx, link) {
if ((inx >= 0) && (inx < this.links.length)) {
this.links[inx] = link;
this.initLinks();
this.writeConfig();
}
}
deleteLink(inx) {
if ((inx >= 0) && (inx < this.switches.length)) {
this.links.splice(inx, 1);
this.initLinks();
this.writeConfig();
}
}
setLink(inx, newstate, newvalue) {
return __awaiter(this, void 0, void 0, function* () {
// find the switch if an index is given
let link = null;
if (typeof inx === "number") {
if ((inx >= 0) && (inx < this.links.length)) {
link = this.links[inx];
}
}
else {
// a Link was passed as first param
link = inx;
}
// check if state is given
if (typeof newstate != "undefined") {
link.unit.status = !!newstate;
}
if (typeof newvalue != "undefined") {
link.unit.value = +newvalue;
}
if (!link) {
(0, logger_1.err)("smartapp", "Didn't find link with inx: " + inx);
}
else if (!link.unit) {
(0, logger_1.err)("smartapp", "Don't have unit for link: " + link.unitname);
}
else {
if (link.type == types_1.LinkType.kAccessory) {
if (yield this.checkToken())
yield (0, HB_1.hbSetState)(this.config.apiHost + ":" + this.config.apiPort, link.id, link.service, link.unit.value, this.token);
}
else {
(0, logger_1.err)("smartapp", "Don't know how to set a link of type " + link.type);
}
}
});
}
//////////////
// Switches //
//////////////
initSwitchUnits() {
(0, logger_1.log)("smartapp", "Init " + this.switches.length + " Switches -> add units");
const smappee = this.power.smappee;
this.switches.forEach(swtch => {
swtch.unit = swtch.unit ||
this.system.findUnit(this.system.findMaster(swtch.masterAddress, swtch.masterPort), swtch.logicalNodeAddress, swtch.logicalAddress);
if ((smappee) && (swtch.type === types_1.SwitchType.kSmappee)) {
for (let key in smappee.plugs) {
// convert to numbers, better be safe then missing one...
const p = (typeof swtch.plug === "string") ? parseInt(swtch.plug) : swtch.plug;
const k = (typeof key === "string") ? parseInt(key) : key;
if (k === p)
swtch.value = smappee.plugs[key].value;
}
;
}
else if ((swtch.type === types_1.SwitchType.kHTTPDimmer) ||
(swtch.type === types_1.SwitchType.kHTTPSwitch) ||
(swtch.type === types_1.SwitchType.kHTTPUpDown)) {
if (swtch.unit) {
swtch.value = swtch.unit.value;
swtch.status = swtch.unit.status;
}
else {
(0, logger_1.log)("smartapp", "** error ** missing unit: " + (0, types_1.hex)(swtch.logicalNodeAddress) + "/" + (0, types_1.hex)(swtch.logicalAddress) + " **");
}
}
});
}
alertSwitch(type, plugNr, value) {
(0, logger_1.debug)("smartapp", "Received " + type + " switch status change: " + plugNr + " -> " + value);
if ((this.power.smappee) && (type === types_1.SwitchType.kSmappee)) {
this.switches.forEach(swtch => {
if ((swtch.type == types_1.SwitchType.kSmappee) && (swtch.plug == plugNr) && swtch.unit) {
(0, logger_1.debug)("smartapp", " -> Switch was attached to unit = " + swtch.unit.getDisplayName() + " -> setting state to " + value);
swtch.unit.setState(value);
}
});
}
}
doSwitches(context) {
return __awaiter(this, void 0, void 0, function* () {
let inx = parseInt(context.id);
let message;
try {
if (context.action === "add") {
this.switches.push(types_1.kEmptySwitch);
inx = this.switches.length - 1;
return this.ejs("switchDetail", context, { config: this.config, swtch: this.switches[inx],
masters: this.system.masters, id: inx });
}
else if (context.action === "edit") {
return this.ejs("switchDetail", context, { rule: types_1.kEmptyRule, swtch: this.switches[inx],
masters: this.system.masters });
}
else if (context.action === "delete") {
this.deleteSwitch(inx);
}
else if (context.action === "change") {
this.updateSwitch(inx, this.scrapeSwitch(context));
}
else if (context.action === "set") {
const state = context.getParam({ name: "state", type: "string", default: "N" });
const value = context.getParam({ name: "value", type: "integer", default: 0 });
this.setSwitch(inx, (state === "Y"), value);
return this.json({ switch: inx, state, value });
}
else {
// possible new IP Nodes, hence Units could be online
this.initSwitchUnits();
}
}
catch (e) {
message = e.toString();
}
return this.ejs("switchList", context, { masters: this.system.masters, switches: this.switches, message });
});
}
informChange(u, kind) {
// kind should be "-", "S"tatus, "M"acro
this.switches.forEach(swtch => {
if (u.isUnit(swtch.masterAddress, swtch.masterPort, swtch.logicalNodeAddress, swtch.logicalAddress)) {
if (!((kind === "M") && (swtch.nomacro === "Y")))
this.setSwitch(swtch);
else
(0, logger_1.debug)("smartapp", "*** macro changed blocked by switch");
}
});
}
scrapeSwitch(context) {
const { name: unitName, masterAddress, masterPort, logicalAddress, logicalNodeAddress } = this.scrapeUnit(context, '');
const plug = context.getParam({ name: "plug", type: "string", default: "0" });
const nomacro = context.getParam({ name: "nomacro", type: "string", default: "N" });
const nostop = context.getParam({ name: "nostop", type: "string", default: "N" });
const stype = context.getParam({ name: "type", type: "string", default: types_1.SwitchType.kNoType });
const name = context.getParam({ name: "name", type: "string", default: "--" });
const data = context.getParam({ name: "data", type: "string", default: "" });
const header = context.getParam({ name: "header", type: "string", default: "" });
const method = context.getParam({ name: "method", type: "string", default: "GET" });
return { name, unitName, masterAddress, masterPort, logicalAddress, logicalNodeAddress, type: stype, plug, data, header, method, nomacro, nostop };
}
updateSwitch(inx, swtch) {
if ((inx >= 0) && (inx < this.switches.length)) {
this.switches[inx] = swtch;
this.initSwitchUnits();
this.writeConfig();
}
}
deleteSwitch(inx) {
if ((inx >= 0) && (inx < this.switches.length)) {
this.switches.splice(inx, 1);
this.initSwitchUnits();
this.writeConfig();
}
}
setSwitch(inx, newstate, newvalue) {
// find the switch if an index is given
let swtch = null;
if (typeof inx === "number") {
if ((inx >= 0) && (inx < this.switches.length)) {
swtch = this.switches[inx];
}
}
else {
// a Switch was passed as first param
swtch = inx;
}
// check if state is given
if (typeof newstate != "undefined") {
swtch.unit.status = !!newstate;
}
if (typeof newvalue != "undefined") {
swtch.unit.value = +newvalue;
}
if (!swtch) {
(0, logger_1.err)("smartapp", "Didn't find switch with inx: " + inx);
}
else if (!swtch.unit) {
(0, logger_1.err)("smartapp", "Don't have unit for switch: " + swtch.unitname);
}
else {
if ((swtch.type === types_1.SwitchType.kSmappee) && (this.power.smappee)) {
this.power.smappee.setPlug(parseInt(swtch.plug), swtch.unit.value);
}
else if (swtch.type === types_1.SwitchType.kHTTPSwitch) {
this.httpSwitch(swtch);
}
else if (swtch.type === types_1.SwitchType.kHTTPDimmer) {
this.httpDimmer(swtch);
}
else if (swtch.type === types_1.SwitchType.kHTTPUpDown) {
this.httpUpDown(swtch);
}
else if (swtch.type === types_1.SwitchType.kOhSwitch) {
this.ohSwitch(swtch);
}
else if (swtch.type === types_1.SwitchType.kOhDimmer) {
this.ohDimmer(swtch);
}
else if (swtch.type === types_1.SwitchType.kOhUpDown) {
this.ohUpDown(swtch);
}
else if (swtch.type === types_1.SwitchType.kSomfy) {
this.somfy(swtch);
}
else {
(0, logger_1.err)("smartapp", "Don't know how to set a switch of type " + swtch.type);
}
}
}
///////////
// Somfy //
///////////
somfy(swtch) {
let nr = swtch.plug;
if (typeof nr === "string")
nr = parseInt(nr);
nr = Math.max(0, Math.min(4, nr));
if (swtch.unit) {
if (swtch.unit.status === 3)
somfy.down(nr); // 3 = going down
else if (swtch.unit.status === 4)
somfy.up(nr); // 4 = going up
else if (swtch.unit.status != 0)
somfy.stop(nr); // 1 = stopped down, 2 = stopped up
}
}
//////////////////////////
// http driven switches //
//////////////////////////
makeVariableURL(url, state, value) {
// support legacy on/off
const parts = url.split("|");
let base = parts[0];
if (parts.length > 2) {
base += parts[state ? 2 : 1];
}
// do the dimmer value and on/off
return base
.replace("#B", state ? "true" : "false")
.replace("#O", state ? "on" : "off")
.replace("#1", state ? '1' : '0')
.replace("#", state ? "on" : "off")
.replace("$B", "" + Math.round(value / 100 * 256))
.replace("$T", "" + Math.round(value / 100 * 512))
.replace("$W", "" + Math.round(value / 100 * 256 * 256))
.replace("$1", "" + (value / 100))
.replace("$", "" + value);
}
httpSwitch(swtch) {
const req = this.makeVariableURL(swtch.plug, !!swtch.unit.status, +swtch.unit.value);
let data = "";
if (swtch.data) {
// we have body data
data = this.makeVariableURL(swtch.data, !!swtch.unit.status, +swtch.unit.value);
}
(0, logger_1.log)("smartapp", "HTTP-Switch(" + !!swtch.unit.status + ") -> " + req);
(0, HA_API_1.httpRequest)(req, swtch.method, data, HA_API_1.ContentType.form, swtch.header);
}
httpUpDown(swtch) {
let url = swtch.plug + "";
// support legacy on/off
const parts = url.split("|");
let base = parts[0];
const val = 1 + swtch.unit.value;
if (val < parts.length) {
base += parts[val];
}
let data = "";
if (swtch.data) {
// we have body data
data = this.makeVariableURL(swtch.data, !!swtch.unit.status, +swtch.unit.value);
}
if ((val === 1) && (swtch.nostop == 'Y')) {
(0, logger_1.debug)("smartapp", "Blocked stop for HTTP-UpDown " + swtch.name);
}
else {
(0, logger_1.log)("smartapp", "UpDown(" + val + ") -> " + base);
(0, HA_API_1.httpRequest)(base, swtch.method, data, HA_API_1.ContentType.form, swtch.header);
}
}
httpDimmer(swtch) {
// do the possible on/off + value part
let req = this.makeVariableURL(swtch.plug, !!swtch.unit.status, +swtch.unit.value);
let data = "";
if (swtch.data) {
// we have body data
data = this.makeVariableURL(swtch.data, !!swtch.unit.status, +swtch.unit.value);
}
(0, logger_1.debug)("smartapp", "Dimmer(" + !!swtch.unit.status + "," + swtch.unit.value + ") -> " + req + " + " + data);
(0, HA_API_1.httpRequest)(req, swtch.method, data, HA_API_1.ContentType.form, swtch.header);
}
////////////////////////
// http request stuff //
////////////////////////
// overrides webapp.login -> login into HB
checkLogin(url, username, password) {
return __awaiter(this, void 0, void 0, function* () {
return (0, HB_1.hbLogin)(url, username, password);
});
}
//////////////
// open HAB //
//////////////
ohSwitch(swtch) {
const req = this.makeVariableURL(swtch.plug, !!swtch.unit.status, +swtch.unit.value);
(0, logger_1.debug)("smartapp", "OH-Switch(" + !!swtch.unit.status + ") -> " + req);
(0, HA_API_1.httpRequest)(req, swtch.method, swtch.unit.status ? "ON" : "OFF", HA_API_1.ContentType.plain, swtch.header);
}
ohDimmer(swtch) {
const req = this.makeVariableURL(swtch.plug, !!swtch.unit.status, +swtch.unit.value);
const val = (swtch.unit.value == 1) ? "ON" : swtch.unit.value.toString();
(0, logger_1.debug)("smartapp", "OH-Dimmer(" + !!swtch.unit.status + ", " + val + ") -> " + req);
(0, HA_API_1.httpRequest)(req, swtch.method, swtch.unit.status ? val : "OFF", HA_API_1.ContentType.plain, swtch.header);
}
ohUpDown(swtch) {
let data;
// 1=stopped, 2-closed, 3=opened, 4=closing, 5=opening
if ((swtch.unit.status == 0) || (swtch.unit.status == 1) || (swtch.unit.status == 2)) {
if (swtch.nostop == 'Y')
(0, logger_1.debug)("smartapp", "Blocked stop for OH-UpDown " + swtch.name);
else
data = "STOP";
}
else if (swtch.unit.status == 3) {
data = "DOWN";
}
else if (swtch.unit.status == 4) {
data = "UP";
}
if (data) {
const req = this.makeVariableURL(swtch.plug, !!swtch.unit.status, +swtch.unit.value);
(0, logger_1.debug)("smartapp", "OH-UpDown(" + swtch.unit.value + ", " + data + ") -> " + req);
(0, HA_API_1.httpRequest)(req, swtch.method, data, HA_API_1.ContentType.plain, swtch.header);
}
}
//////////////////////////////
// Services //
//////////////////////////////
doServices(context) {
return __awaiter(this, void 0, void 0, function* () {
let units = this.system.allUsedUnits();
for (let u of units) {
yield u.node.master.requestUnitStatus(u);
}
return this.ejs("serviceList", context, { units, nrActive: this.system.allActiveUnits().length });
});
}
//////////////////////////////
// Masters //
//////////////////////////////
doMasters(context) {
return __awaiter(this, void 0, void 0, function* () {
let message;
try {
if (context.action === "new") {
return this.ejs("masterDetail", context, { config: types_1.Sanitizers.masterConfig(null), registers: [], nodes: [] });
}
else if (context.action === "list") {
return this.ejs("unitList", context, { masters: this.system.masters });
}
else if (context.action === "services") {
return this.serviceList(context);
}
else if (context.action === "edit") {
context.getMaster("id");
const master = this.system.findMaster(context["masterAddress"], context["masterPort"]);
if (master)
return this.ejs("masterDetail", context, { nodes: master.nodes, config: master.getConfig(), registers: master.registers });
else
message = "Error: Master not found";
}
else if (context.action === "delete") {
const master = this.system.findMaster(context.getParam(kAddress), context.getParam(kPort));
if (master)
yield this.system.deleteMaster(master);
else
message = "Error: Master not found";
// drop through and list all masters
}
else if (context.action === "save") {
const master = yield this.system.addMaster(context.getParam(kAddressOld), context.getParam(kPortOld), { address: context.getParam(kAddress), port: context.getParam(kPort),
password: context.getParam(kPassword), name: context.getParam(kName),
active: context.getParam(kActive) != "N", nodenames: {} });
this.updateNodes(master, context.params.nodes || "", context.params);
this.system.writeConfig();
}
else if (context.action === "close") {
context.getMaster("id");
const master = this.system.findMaster(context["masterAddress"], context["masterPort"]);
if (master)
yield master.close(false);
else
message = "Error: Master not found";
}
}
catch (e) {
message = e.toString();
}
return this.ejs("masterList", context, { masters: this.system.masters, message });
});
}
serviceList(context) {
return __awaiter(this, void 0, void 0, function* () {
let units = this.system.allActiveUnits();
for (let u of units) {
yield u.node.master.requestUnitStatus(u);
}
return this.ejs("unitList", context, { services: this.system.allActiveUnits() });
});
}
updateNodes(master, nodes, params) {
return __awaiter(this, void 0, void 0, function* () {
if (nodes) {
let nodeArr = nodes.map(s => parseInt(s));
nodeArr.forEach(adr => {
let node = this.system.findNode(master, adr);
if (node) {
node.active = (params["active_" + adr] === "Y");
this.system.setActiveState(node);
}
});
}
});
}
//////////////////////////////
// Units //
//////////////////////////////
doUnits(context) {
return __awaiter(this, void 0, void 0, function* () {
// get masterAddress and Port
context.getMaster("action", "node");
const master = this.system.findMaster(context["masterAddress"], context["masterPort"]);
if (context.action === "save") {
// store changes in units into master config
this.updateUnits(master, context.getParam(kNode), context.params);
this.system.writeConfig();
context.action = "cancel";
}
if (context.action === "cancel") {
// return to previous screen -> show master info + list of nodes.
context.id = context["masterAddress"] + ":" + context["masterPort"];
context.request = "masters";
context.action = "edit";
return this.doRequest(context);
}
else if (context.action === "set") {
if (!master)
return this.error(context, "master not found", true);
const { logicalNodeAddress, logicalAddress } = context.getUnit();
const response = yield this.setState(master, logicalNodeAddress, logicalAddress, context.getParam(kValue));
return this.json(response);
}
else if (context.action === "get") {
if (!master)
return this.error(context, "master not found", true);
const { logicalNodeAddress, logicalAddress } = context.getUnit();
const response = yield this.getState(master, logicalNodeAddress, logicalAddress);
return this.json(response);
}
else if (context.action === "press") {
if (!master)
return this.error(context, "master not found", true);
const { logicalNodeAddress, logicalAddress } = context.getUnit();
const response = yield this.doPress(master, logicalNodeAddress, logicalAddress, context.getParam(kIntValue));
return this.json(response);
}
else { // context.action === "node"
// reponding to /units/[master ip address:port]/[logical node address]
const nodeLogicalAddress = parseInt(context.id);
let response = yield this.getNodeInfo(master, nodeLogicalAddress, true);
this.adjustUnitsToConfig(response.node);
let N = this.sortCopy(response.node, context);
return this.ejs("nodeDetail", context, { message: response.message, node: N });
}
});
}
adjustUnitsToConfig(node) {
// check if units are still in config
node.units.forEach(unit => {
//console.log("Searching: ", unit.node.master.config.address, unit.node.master.config.port, unit.logicalNodeAddress, unit.logicalAddress)
const config = this.system.config.cunits.find(u => {
// console.log(u.masterAddress, u.masterPort