homebridge-smartsystem
Version:
SmartServer (Proxy TCP sockets to the cloud, Smappee MQTT, Duotecno IP Nodes, Homekit interface)
1,062 lines • 73.9 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 WebSocket = require("ws");
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
const configuredPort = this.config.port || 80;
this.password = platform.config.password || this.config.password || "";
this.user = platform.config.user || this.config.user || "pi";
// Set up multi-port listening if port is not 80
// Note: Port 80 requires root/sudo on Unix-like systems
// For development on macOS/Linux without sudo, use only configured port
if (configuredPort !== 80) {
// Check if we're running with appropriate privileges (process.getuid exists only on Unix)
const canBindPort80 = process.platform === 'win32' || (process.getuid && process.getuid() === 0);
if (canBindPort80) {
(0, logger_1.log)("smartapp", `Configured for dual-port mode: ${configuredPort} and 80`);
this.port = [80, configuredPort];
}
else {
(0, logger_1.log)("smartapp", `Running without root privileges - listening only on port ${configuredPort}`);
(0, logger_1.log)("smartapp", `To enable port 80, run with sudo or use: sudo setcap 'cap_net_bind_service=+ep' $(which node)`);
this.port = configuredPort;
}
}
else {
this.port = 80;
}
// Initialize mDNS service
this.setupMDNS();
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.system.emitter.on('ready', this.initDeviceUnits.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("deviceDetail", __dirname + "/views/device-details.ejs", "text/html");
this.addFile("deviceList", __dirname + "/views/device-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("logoWhite", __dirname + "/views/assets/Duotecno_logo_white.svg", "image/svg+xml");
this.addFile("proxy", __dirname + "/views/proxy.ejs", "text/html");
}
// Override to use config.system.debug for template caching control
isDebugMode() {
var _a, _b;
return ((_b = (_a = this.system) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.debug) === true;
}
setupMDNS() {
// Read settings to get configured mDNS name
const settings = this.read("settings");
const mdnsName = (settings === null || settings === void 0 ? void 0 : settings.mdnsName) || "duotecno-gateway";
const version = require('../package.json').version;
// Initialize mDNS through parent class
this.initMDNS(mdnsName, version);
}
serve(onConnected) {
// Initialize WebSocket servers array
this.wsServers = [];
// Call parent serve method
super.serve(() => {
// Set up WebSocket servers on the HTTP servers
this.setupWebSocketServers();
// Register mDNS service after server is listening
this.registerMDNS();
// Call the original callback if provided
if (onConnected) {
onConnected();
}
});
}
setupWebSocketServers() {
// Get all HTTP servers
this.servers.forEach((httpServer, index) => {
const wss = new WebSocket.Server({ server: httpServer, path: '/devices' });
this.wsServers.push(wss);
wss.on('connection', (ws) => {
(0, logger_1.log)("smartapp", `WebSocket client connected on server ${index}`);
// Send initial device list
this.sendDeviceList(ws);
ws.on('message', (message) => {
try {
const data = JSON.parse(message.toString());
this.handleDeviceMessage(ws, data);
}
catch (e) {
(0, logger_1.err)("smartapp", `Invalid WebSocket message: ${e.message}`);
}
});
ws.on('close', () => {
(0, logger_1.log)("smartapp", `WebSocket client disconnected from server ${index}`);
});
ws.on('error', (error) => {
(0, logger_1.err)("smartapp", `WebSocket error on server ${index}: ${error.message}`);
});
});
(0, logger_1.log)("smartapp", `WebSocket server ${index} set up on path /devices`);
});
}
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));
this.config.devices = this.devices.map(d => types_1.Sanitizers.makeDeviceConfig(d));
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));
this.devices = (this.config.devices || []).map(d => types_1.Sanitizers.deviceConfig(d));
}
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.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 === "devices") {
return this.doDevices(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.read("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.configPort = context.getParam({ name: "configPort", type: "integer" });
config.masterAddress = context.getParam({ name: "masterAddress", type: "string" });
config.masterPort = context.getParam({ name: "masterPort", type: "integer" });
config.masterConfigPort = context.getParam({ name: "masterConfigPort", type: "integer" });
config.uniqueId = context.getParam({ name: "uniqueId", type: "string" });
config.configUniqueId = context.getParam({ name: "configUniqueId", type: "string" });
this.write("proxy", config);
(0, proxy_1.setProxyConfig)(config);
// Automatically restart proxy with new config
(0, proxy_1.cleanStart)(true);
}
else if (context.action === "restart") {
(0, proxy_1.cleanStart)(true);
}
else {
// just show the proxy config
}
// Get mDNS status for display
const mdnsStatus = this.mdnsService ? {
registeredName: this.mdnsService.registeredName,
registrationError: this.mdnsService.registrationError,
registrationStatus: this.mdnsService.registrationStatus
} : null;
return this.ejs("proxy", context, { config, connections: proxy_1.gCloudConnections, mdnsStatus });
});
}
//////////////
// Settings //
//////////////
doSettings(context) {
return __awaiter(this, void 0, void 0, function* () {
let config = this.read("settings");
let message = "";
if (context.action === "save") {
config = this.scrapeSettings(context);
this.write("settings", config);
message = "Settings saved successfully. Restart the server for changes to take effect.";
// Re-register mDNS with new name if changed
if (this.mdnsService) {
// Note: Full restart recommended for mDNS changes to take effect properly
(0, logger_1.log)("smartapp", "Settings saved. mDNS name updated to: " + config.mdnsName);
}
}
else if (context.action === "restart") {
context.request = "restart";
return this.doRestart(false);
}
else {
// Just display the settings form
}
return this.ejs("settings", context, { config, message });
});
}
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" })
},
mdnsName: context.getParam({ name: "mdnsName", type: "string", default: "duotecno-gateway" })
});
}
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") {
try {
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);
}
}
catch (e) {
(0, logger_1.err)("smartapp", "informLinks: failed to set HB value for accessory " + link.accName + " -> " + e.toString());
}
}
}
}
;
});
}
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();
}
}
/////////////
// Devices //
/////////////
initDeviceUnits() {
(0, logger_1.log)("smartapp", "Init " + this.devices.length + " Devices -> add units");
this.devices.forEach(device => {
device.unit || (device.unit = this.system.findUnit(this.system.findMaster(device.masterAddress, device.masterPort), device.logicalNodeAddress, device.logicalAddress));
if (device.unit) {
device.relay = device.unit.status > 0;
}
else {
(0, logger_1.log)("smartapp", "** error ** missing device unit: " + (0, types_1.hex)(device.logicalNodeAddress) + "/" + (0, types_1.hex)(device.logicalAddress) + " **");
}
});
}
doDevices(context) {
return __awaiter(this, void 0, void 0, function* () {
let inx = parseInt(context.id);
const units = this.system.allUsedUnits();
let message;
try {
if (context.action === "add") {
this.devices.push(Object.assign(Object.assign({}, types_1.kEmptyDevice), { measured: false }));
inx = this.devices.length - 1;
return this.ejs("deviceDetail", context, { device: this.devices[inx], id: inx,
masters: this.system.masters, units });
}
else if (context.action === "edit") {
return this.ejs("deviceDetail", context, { device: this.devices[inx], id: inx,
masters: this.system.masters, units });
}
else if (context.action === "delete") {
this.deleteDevice(inx);
}
else if (context.action === "change") {
this.updateDevice(inx, this.scrapeDevice(context));
}
else {
// possible new IP Nodes, hence Units could be online
this.initDeviceUnits();
}
}
catch (e) {
message = e.toString();
}
return this.ejs("deviceList", context, { masters: this.system.masters, devices: this.devices, message });
});
}
scrapeDevice(context) {
const { masterAddress, masterPort, logicalAddress, logicalNodeAddress } = this.scrapeUnit(context, '');
const name = context.getParam({ name: "name", type: "string", default: "Device" });
const powerSource = context.getParam({ name: "powerSource", type: "string", default: "none" });
const powerChannel = context.getParam({ name: "powerChannel", type: "string", default: "" });
const estimatedPower = context.getParam({ name: "estimatedPower", type: "integer", default: 0 });
// Calculate ID from node/unit
const id = logicalNodeAddress * 256 + logicalAddress;
// Measured is true if powerSource is not "none"
const measured = powerSource !== "none";
return {
id,
name,
masterAddress,
masterPort,
logicalAddress,
logicalNodeAddress,
measured,
powerSource: powerSource,
powerChannel,
estimatedPower,
power: 0,
energy: 0,
voltage: 230.0,
current: 0,
relay: false
};
}
updateDevice(inx, device) {
if ((inx >= 0) && (inx < this.devices.length)) {
this.devices[inx] = device;
this.initDeviceUnits();
this.writeConfig();
}
}
deleteDevice(inx) {
if ((inx >= 0) && (inx < this.devices.length)) {
this.devices.splice(inx, 1);
this.initDeviceUnits();
this.writeConfig();
}
}
//////////////////////
// Device - sockets //
//////////////////////
sendDeviceList(ws) {
const devices = this.getDeviceList();
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(devices));
}
}
getDeviceList() {
return this.devices.map(device => {
// Get current relay state from unit if available
let relayState = device.relay || false;
if (device.unit) {
relayState = device.unit.status > 0;
}
// Get power measurements based on power source
let power = 0;
let energy = 0;
let voltage = 230.0;
let current = 0;
let measured = false;
if (device.powerSource === types_1.PowerSource.kSmappee && this.power.smappee) {
// Get power from Smappee
const smappeePower = this.getPowerFromSmappee(device.powerChannel);
if (smappeePower !== null) {
power = smappeePower;
measured = true;
current = power / voltage;
}
}
else if (device.powerSource === types_1.PowerSource.kP1 && this.power.p1) {
// Get power from P1 meter
const p1Power = this.getPowerFromP1(device.powerChannel);
if (p1Power !== null) {
power = p1Power;
measured = true;
current = power / voltage;
}
}
else if (device.powerSource === types_1.PowerSource.kShelly && this.power.shelly) {
// Get power from Shelly
const shellyPower = this.getPowerFromShelly(device.powerChannel);
if (shellyPower !== null) {
power = shellyPower;
measured = true;
current = power / voltage;
}
}
else if (device.powerSource === types_1.PowerSource.kNone || !device.powerSource) {
// Use estimated power when relay is on
power = relayState ? (device.estimatedPower || 0) : 0;
measured = false;
}
return {
id: device.id,
name: device.name,
power: power,
energy: energy,
voltage: voltage,
current: current,
relay: relayState,
measured: measured
};
});
}
getPowerFromSmappee(channel) {
if (!this.power.smappee)
return null;
try {
// Channel format could be "0", "0+1", "0+1+2", etc.
const channels = channel.split('+').map(c => parseInt(c.trim()));
let totalPower = 0;
for (const ch of channels) {
const binding = this.power.smappee.bindings.find(b => b.channel.includes(ch.toString()));
if (binding && binding.value !== undefined) {
totalPower += binding.value;
}
}
return totalPower;
}
catch (e) {
(0, logger_1.err)("smartapp", `Error getting Smappee power for channel ${channel}: ${e.message}`);
return null;
}
}
getPowerFromP1(channel) {
if (!this.power.p1)
return null;
try {
// P1 meter typically has single power value
const binding = this.power.p1.bindings.find(b => b.channel.includes(channel));
if (binding && binding.value !== undefined) {
return binding.value;
}
return null;
}
catch (e) {
(0, logger_1.err)("smartapp", `Error getting P1 power for channel ${channel}: ${e.message}`);
return null;
}
}
getPowerFromShelly(channel) {
if (!this.power.shelly)
return null;
try {
// Shelly channel format similar to Smappee
const channels = channel.split('+').map(c => parseInt(c.trim()));
let totalPower = 0;
for (const ch of channels) {
const binding = this.power.shelly.bindings.find(b => b.channel.includes(ch.toString()));
if (binding && binding.value !== undefined) {
totalPower += binding.value;
}
}
return totalPower;
}
catch (e) {
(0, logger_1.err)("smartapp", `Error getting Shelly power for channel ${channel}: ${e.message}`);
return null;
}
}
handleDeviceMessage(ws, data) {
return __awaiter(this, void 0, void 0, function* () {
if (!Array.isArray(data)) {
(0, logger_1.err)("smartapp", "WebSocket message must be an array");
return;
}
// Empty array = keep-alive / poll
if (data.length === 0) {
this.sendDeviceList(ws);
return;
}
// Process relay control commands
for (const cmd of data) {
if (cmd.id && cmd.relay !== undefined) {
yield this.setDeviceRelay(cmd.id, cmd.relay);
}
}
// Send updated device list
this.sendDeviceList(ws);
});
}
setDeviceRelay(id, state) {
return __awaiter(this, void 0, void 0, function* () {
// Convert ID to node/unit (id = node * 256 + unit)
const node = Math.floor(id / 256);
const unit = id % 256;
(0, logger_1.log)("smartapp", `Setting device ${id} (node=${node}, unit=${unit}) to ${state}`);
// Get the first master
const master = this.system.masters[0];
if (!master) {
(0, logger_1.err)("smartapp", "No master available");
return;
}
// Find the unit
const nodeObj = master.nodes.find(n => n.logicalAddress === node);
if (!nodeObj) {
(0, logger_1.err)("smartapp", `Node ${node} not found`);
return;
}
const unitObj = nodeObj.units.find(u => u.logicalAddress === unit);
if (!unitObj) {
(0, logger_1.err)("smartapp", `Unit ${unit} in node ${node} not found`);
return;
}
// Set the unit state (convert boolean to 0/100 for dimmer/switch)
yield master.setUnitStatus(unitObj, state ? 100 : 0);
});
}
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 st