homebridge-smartsystem 7.1.26 → 7.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.setHomeServerCfg = exports.UDP_CONFIG_PORT = void 0;
13
+ // Minimal client for Duotecno's "UDP config tool" protocol -- just enough to
14
+ // push a new home-server (master) password/port to a master over UDP.
15
+ //
16
+ // Wire format reverse-engineered from the real `dtudpconfigLIB` source (see
17
+ // the Duotecno Designer project's src/services/udpConfig/protocol.ts for the
18
+ // full writeup and provenance notes). Devices listen on UDP port 8002.
19
+ // ASCII framing: `[b0,b1,...]` + LF, every byte its decimal string,
20
+ // comma-separated. Strings are length-prefixed (one length byte, then that
21
+ // many byte-per-char, no null terminator). Multi-byte ints are MSB-first.
22
+ const dgram = require("dgram");
23
+ exports.UDP_CONFIG_PORT = 8002;
24
+ const CMD_FC_APPCONFIG = 189;
25
+ const CMD_EV_APPCONFIG = 15;
26
+ const ATTR_HOMESRVCFG_SET = 2;
27
+ // Always written as "closed + open protocol enabled", matching the legacy
28
+ // config tool -- it's a read-only display value, not a user-editable input.
29
+ const HOMESRVCFG_PROTOCOL_FLAGS = 0x03;
30
+ function encodePacket(bytes) {
31
+ return Buffer.from(`[${bytes.join(",")}]\n`, "ascii");
32
+ }
33
+ function decodePacket(buf) {
34
+ const str = buf.toString("ascii").trim();
35
+ const match = /^\[([^\]]*)\]/.exec(str);
36
+ if (!match || !match[1])
37
+ return [];
38
+ return match[1].split(",").map((s) => parseInt(s.trim(), 10));
39
+ }
40
+ function stringToBytes(s) {
41
+ return [s.length, ...Array.from(s).map((c) => c.charCodeAt(0))];
42
+ }
43
+ function u16ToBytes(v) {
44
+ return [(v >> 8) & 0xff, v & 0xff];
45
+ }
46
+ /** Sends one unicast packet to ip:8002 and waits for a matching reply, retrying on timeout (UDP can drop packets). */
47
+ function request(ip, packetBytes, expectCmd, expectMethod, timeoutMs = 1500, retries = 2) {
48
+ return new Promise((resolve, reject) => {
49
+ const socket = dgram.createSocket({ type: "udp4", reuseAddr: true });
50
+ let attemptsLeft = retries;
51
+ let timer;
52
+ const cleanup = () => {
53
+ clearTimeout(timer);
54
+ socket.close();
55
+ };
56
+ socket.on("error", (err) => {
57
+ cleanup();
58
+ reject(err);
59
+ });
60
+ socket.on("message", (msg) => {
61
+ const data = decodePacket(msg);
62
+ if (data[0] === expectCmd && data[1] === expectMethod) {
63
+ cleanup();
64
+ resolve(data);
65
+ }
66
+ });
67
+ function send() {
68
+ socket.send(encodePacket(packetBytes), exports.UDP_CONFIG_PORT, ip);
69
+ timer = setTimeout(() => {
70
+ if (attemptsLeft-- > 0)
71
+ send();
72
+ else {
73
+ cleanup();
74
+ reject(new Error(`No UDP config reply from ${ip} (cmd ${expectCmd}/${expectMethod})`));
75
+ }
76
+ }, timeoutMs);
77
+ }
78
+ socket.bind(0, send);
79
+ });
80
+ }
81
+ /** Pushes a new home-server password/TCP-port to a master over the UDP config protocol (port 8002). */
82
+ function setHomeServerCfg(ip, password, ipPort, timeoutMs) {
83
+ return __awaiter(this, void 0, void 0, function* () {
84
+ const packet = [CMD_FC_APPCONFIG, ATTR_HOMESRVCFG_SET, ...stringToBytes(password), ...u16ToBytes(ipPort), ...u16ToBytes(HOMESRVCFG_PROTOCOL_FLAGS)];
85
+ const reply = yield request(ip, packet, CMD_EV_APPCONFIG, ATTR_HOMESRVCFG_SET, timeoutMs);
86
+ return reply[2] === 1;
87
+ });
88
+ }
89
+ exports.setHomeServerCfg = setHomeServerCfg;
90
+ //# sourceMappingURL=udpConfig.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-smartsystem",
3
3
  "displayname": "Duotecno Bridge",
4
- "version": "7.1.26",
4
+ "version": "7.2.1",
5
5
  "description": "SmartServer (Proxy TCP sockets to the cloud, Smappee MQTT, Duotecno IP Nodes, Homekit interface)",
6
6
  "main": "index.js",
7
7
  "author": "Johan Coppieters",
@@ -27,6 +27,7 @@ const somfy = require("./somfy");
27
27
  const fs_1 = require("fs");
28
28
  const overkiz_1 = require("./overkiz");
29
29
  const config_1 = require("../duotecno/config");
30
+ const udpConfig_1 = require("../duotecno/udpConfig");
30
31
  const HB_1 = require("./HB");
31
32
  const HA_API_1 = require("./HA-API");
32
33
  const proxy_1 = require("./proxy");
@@ -74,7 +75,7 @@ class SmartApp extends webapp_1.WebApp {
74
75
  // get some configurated params
75
76
  const configuredPort = this.config.port || 80;
76
77
  this.password = platform.config.password || this.config.password || "";
77
- this.user = platform.config.user || this.config.user || "pi";
78
+ this.user = platform.config.user || this.config.user || "duotecno";
78
79
  // Set up multi-port listening if port is not 80
79
80
  // Note: Port 80 requires root/sudo on Unix-like systems
80
81
  // For development on macOS/Linux without sudo, use only configured port
@@ -794,8 +795,22 @@ function SmartSocketInit(message){
794
795
  yield this.informOverkizUpDown(u, link);
795
796
  return;
796
797
  }
797
- // dimmer: dt2hbvalue reports the raw 0-100 intensity directly, no
798
- // real/virtual distinction to worry about here (unlike up/down below).
798
+ // A dimmer (or, for a Screen/Awning target, a mood carrying a numeric
799
+ // macro payload -- the same "set to X%" trick previously used to park a
800
+ // screen/awning at a specific height) linked to a Screen/Awning sets its
801
+ // position; linked to a Light it sets intensity instead. Dispatch on the
802
+ // Overkiz device's own type, not just the Duotecno unit's type, since a
803
+ // dimmer unit is now also how a real Overkiz dimmer (Light) gets driven.
804
+ const targetUiClass = this.power.overkiz.uiClass(link.accId);
805
+ const isPositional = (targetUiClass === "Screen") || (targetUiClass === "Awning");
806
+ if (isPositional && (u.isDimmer() || u.isMood())) {
807
+ yield this.informOverkizPosition(u, link);
808
+ return;
809
+ }
810
+ if (!u.isDimmer())
811
+ return;
812
+ // dt2hbvalue reports the raw 0-100 intensity directly, no real/virtual
813
+ // distinction to worry about here (unlike up/down above).
799
814
  const hbValue = (0, HB_1.dt2hbvalue)(u, link.hbValue, link.min, link.max);
800
815
  if ((typeof hbValue === "undefined") || (hbValue === link.hbValue))
801
816
  return;
@@ -809,6 +824,32 @@ function SmartSocketInit(message){
809
824
  }
810
825
  });
811
826
  }
827
+ // Sets a Screen/Awning to a specific position (0-100) from a dimmer's raw
828
+ // value, or from a mood unit's value when Duotecno programming sends it a
829
+ // numeric macro payload (the same mechanism dimmers use -- a mood isn't
830
+ // normally more than an on/off trigger, but if its macro carries a value
831
+ // it ends up in unit.value exactly like a dimmer's does). Our scale is
832
+ // 0=off/closed, 100=on/fully open (matching how dimmer links always
833
+ // worked here); Overkiz's closure/deployment scale is the opposite
834
+ // (0=open, 100=closed), so invert on the way out.
835
+ informOverkizPosition(u, link) {
836
+ return __awaiter(this, void 0, void 0, function* () {
837
+ if (!this.power.overkiz)
838
+ return;
839
+ const raw = (typeof u.value === "boolean") ? (u.value ? 100 : 0) : Math.round(u.value);
840
+ const position = Math.max(0, Math.min(100, raw));
841
+ if (position === link.hbValue)
842
+ return;
843
+ try {
844
+ link.hbValue = position;
845
+ (0, logger_1.log)("smartapp", "-> Setting Overkiz position for: " + link.accName + " to " + position + "% of Duotecno unit: " + u.getDisplayName());
846
+ yield this.power.overkiz.setClosure(link.accId, 100 - position);
847
+ }
848
+ catch (e) {
849
+ (0, logger_1.err)("smartapp", "informOverkizPosition: failed for " + link.accName + " -> " + e.toString());
850
+ }
851
+ });
852
+ }
812
853
  // A Duotecno up/down (kSwitchingMotor) unit reports status two different
813
854
  // ways depending on how it's wired in the Duotecno programming:
814
855
  // - a "virtual button" duoswitch macro event -> unit.value = 10 (stop) /
@@ -888,10 +929,27 @@ function SmartSocketInit(message){
888
929
  if (!this.power.overkiz || (this.power.overkiz === this.attachedOverkiz))
889
930
  return;
890
931
  this.attachedOverkiz = this.power.overkiz;
891
- this.power.overkiz.emitter.on("state", (deviceURL, unitState) => {
932
+ this.power.overkiz.emitter.on("state", (deviceURL, unitState, closure) => {
892
933
  for (const link of this.links) {
893
934
  if ((link.type !== types_1.LinkType.kOverkiz) || (link.accId !== deviceURL) || !link.unit)
894
935
  continue;
936
+ if (link.unit.isDimmer()) {
937
+ // position-sourced link: reflect Overkiz's position back as a
938
+ // dimmer value. Overkiz's closure/deployment scale is the opposite
939
+ // of ours (0=open/100=closed vs our 0=off/100=on) -- invert back.
940
+ const position = Math.max(0, Math.min(100, Math.round(100 - closure)));
941
+ if (position === link.dtValue)
942
+ continue;
943
+ link.dtValue = position;
944
+ (0, logger_1.log)("smartapp", "-> Setting value of Duotecno unit: " + link.unit.getDisplayName() + " to " + position + " from Overkiz position " + closure + " of: " + link.accName);
945
+ link.unit.setState(position);
946
+ continue;
947
+ }
948
+ // a mood can only be triggered, not set to an arbitrary value, so a
949
+ // mood-sourced position link is Duotecno -> Overkiz only; nothing to
950
+ // reflect back here. Only an up/down unit gets the state below.
951
+ if (!link.unit.isUpDown())
952
+ continue;
895
953
  if (unitState === link.dtValue)
896
954
  continue; // no repeat
897
955
  link.dtValue = unitState;
@@ -1674,7 +1732,7 @@ function SmartSocketInit(message){
1674
1732
  let message;
1675
1733
  try {
1676
1734
  if (context.action === "new") {
1677
- return this.ejs("masterDetail", context, { config: types_1.Sanitizers.masterConfig(null), registers: [], nodes: [] });
1735
+ return this.ejs("masterDetail", context, { config: types_1.Sanitizers.masterConfig(null), registers: [], nodes: [], loggedIn: false });
1678
1736
  }
1679
1737
  else if (context.action === "list") {
1680
1738
  return this.ejs("unitList", context, { masters: this.system.masters });
@@ -1686,7 +1744,7 @@ function SmartSocketInit(message){
1686
1744
  context.getMaster("id");
1687
1745
  const master = this.system.findMaster(context["masterAddress"], context["masterPort"]);
1688
1746
  if (master)
1689
- return this.ejs("masterDetail", context, { nodes: master.nodes, config: master.getConfig(), registers: master.registers });
1747
+ return this.ejs("masterDetail", context, { nodes: master.nodes, config: master.getConfig(), registers: master.registers, loggedIn: master.isLoggedIn });
1690
1748
  else
1691
1749
  message = "Error: Master not found";
1692
1750
  }
@@ -1713,6 +1771,27 @@ function SmartSocketInit(message){
1713
1771
  else
1714
1772
  message = "Error: Master not found";
1715
1773
  }
1774
+ else if (context.action === "changepw") {
1775
+ const master = this.system.findMaster(context.getParam(kAddressOld), context.getParam(kPortOld));
1776
+ if (!master)
1777
+ message = "Error: Master not found";
1778
+ else if (!master.isLoggedIn)
1779
+ message = "Error: not logged in to this master -> refused to change its password";
1780
+ else {
1781
+ const address = context.getParam(kAddress);
1782
+ const port = context.getParam(kPort);
1783
+ const password = context.getParam(kPassword);
1784
+ if (yield (0, udpConfig_1.setHomeServerCfg)(address, password, port)) {
1785
+ const updated = yield this.system.addMaster(context.getParam(kAddressOld), context.getParam(kPortOld), { address, port, password, name: context.getParam(kName),
1786
+ active: context.getParam(kActive) != "N", nodenames: {} });
1787
+ this.updateNodes(updated, context.params.nodes || "", context.params);
1788
+ this.system.writeConfig();
1789
+ message = "Password changed on master and saved";
1790
+ }
1791
+ else
1792
+ message = "Error: master refused the new password";
1793
+ }
1794
+ }
1716
1795
  }
1717
1796
  catch (e) {
1718
1797
  message = e.toString();
@@ -45,6 +45,16 @@
45
45
  <label for="password">Password</label>
46
46
  </div>
47
47
  </div>
48
+ <% if (typeof nodes !== "undefined") { %>
49
+ <div class="row">
50
+ <div class="col s12">
51
+ <button class="btn waves-effect waves-light" type="submit" name="action" value="changepw" <%= loggedIn ? '' : 'disabled' %>>Change Password on Master</button>
52
+ <% if (!loggedIn) { %>
53
+ <span class="grey-text"><small>Only available while logged in to this master.</small></span>
54
+ <% } %>
55
+ </div>
56
+ </div>
57
+ <% } %>
48
58
  <div class="row">
49
59
  <div class="col s6">
50
60
  Active
@@ -46,11 +46,11 @@
46
46
  <li <% if (request === "proxy") { %>class="active"<% } %>><a href="<%= basePath %>/proxy">Proxy</a></li>
47
47
  <li><div class="divider"></div></li>
48
48
  <li class="subheader">Linking</li>
49
- <li <% if (request === "switches") { %>class="active"<% } %>><a href="<%= basePath %>/switches">Http Switches</a></li>
50
- <li <% if (request === "links") { %>class="active"<% } %>><a href="<%= basePath %>/links">Accessory Links</a></li>
49
+ <li <% if (request === "switches") { %>class="active"<% } %>><a href="<%= basePath %>/switches">Http</a></li>
50
+ <li <% if (request === "links") { %>class="active"<% } %>><a href="<%= basePath %>/links">Accessory</a></li>
51
51
  <li><div class="divider"></div></li>
52
52
  <li class="subheader">Energy</li>
53
- <li <% if (request === "devices") { %>class="active"<% } %>><a href="<%= basePath %>/devices">Websocket devices</a></li>
53
+ <li <% if (request === "devices") { %>class="active"<% } %>><a href="<%= basePath %>/devices">Julix</a></li>
54
54
  <% if (hasSmappee) { %><li <% if ((request === "power") && (type === "smappee")) { %>class="active"<% } %>><a href="<%= basePath %>/power?type=smappee">Smappee</a></li><% } %>
55
55
  <% if (hasP1) { %><li <% if ((request === "power") && (type === "p1")) { %>class="active"<% } %>><a href="<%= basePath %>/power?type=p1">P1 Meter</a></li><% } %>
56
56
  <% if (hasShelly) { %><li <% if ((request === "power") && (type === "shelly")) { %>class="active"<% } %>><a href="<%= basePath %>/power?type=shelly">Shelly PM</a></li><% } %>