UNPKG

homebridge-smartsystem

Version:

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

1,062 lines 73.9 kB
"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