homebridge-smartsystem 7.1.25 → 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.
@@ -8,6 +8,7 @@
8
8
  "sourceMaps": true,
9
9
  "name": "Test HB Platform",
10
10
  "program": "${workspaceFolder}/testHB.js",
11
+ "runtimeExecutable": "/Users/johan/.nvm/versions/node/v20.19.5/bin/node",
11
12
  "skipFiles": [
12
13
  "<node_internals>/**"
13
14
  ]
@@ -19,6 +20,7 @@
19
20
  "sourceMaps": true,
20
21
  "name": "Run SmartSocket Proxy",
21
22
  "program": "${workspaceFolder}/testProxy.js",
23
+ "runtimeExecutable": "/Users/johan/.nvm/versions/node/v20.19.5/bin/node",
22
24
  "skipFiles": [
23
25
  "<node_internals>/**"
24
26
  ]
@@ -30,6 +32,7 @@
30
32
  "sourceMaps": true,
31
33
  "name": "Run Test Websockets",
32
34
  "program": "${workspaceFolder}/testWS.js",
35
+ "runtimeExecutable": "/Users/johan/.nvm/versions/node/v20.19.5/bin/node",
33
36
  "skipFiles": [
34
37
  "<node_internals>/**"
35
38
  ]
package/duotecno/types.js CHANGED
@@ -167,6 +167,7 @@ var LinkType;
167
167
  (function (LinkType) {
168
168
  LinkType["kNoType"] = "";
169
169
  LinkType["kAccessory"] = "accessory";
170
+ LinkType["kOverkiz"] = "overkiz";
170
171
  })(LinkType = exports.LinkType || (exports.LinkType = {}));
171
172
  exports.kEmptyLink = Object.assign(Object.assign({}, exports.kEmptyUnit), { type: LinkType.kNoType, max: 100, min: 0, nostop: "N", accName: "acc", accId: "", accService: [] });
172
173
  /////////////
@@ -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.25",
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",
package/server/overkiz.js CHANGED
@@ -8,11 +8,32 @@
8
8
  // then polling its HomeKit accessories back into Duotecno via server/HB.ts
9
9
  // and the Link mechanism in smartapp.ts.
10
10
  //
11
- // Phase 1 (this file): connect, discover devices, send open/close/stop
12
- // commands, and emit translated Duotecno-style states when Overkiz reports
13
- // a change. Wiring this into the Link UI (so a Duotecno unit can be coupled
14
- // to a specific Somfy screen) is a follow-up step once connectivity against
15
- // real hardware is confirmed.
11
+ // Connects, discovers devices, sends open/close/stop/dim commands, and emits
12
+ // translated Duotecno-style states when Overkiz reports a change. Wired into
13
+ // the Link UI in smartapp.ts so a Duotecno up/down or dimmer unit can be
14
+ // coupled to a specific Overkiz device.
15
+ //
16
+ // Verified against real hardware (a TaHoma box with Screen and Awning
17
+ // devices): both widget types expose a genuine 'stop' command, unlike the
18
+ // Homebridge-accessory link path (server/HB.ts) which has to fake a stop by
19
+ // re-setting the last known (and often unreliable) position.
20
+ //
21
+ // Screen devices (uiClass "Screen", e.g. roller shutters/blinds) use
22
+ // open/close/stop/setClosure. Awning devices (uiClass "Awning", e.g. sun
23
+ // awnings) use a different command set: undeploy/deploy/stop/setDeployment
24
+ // (there is no "open"/"close" on an Awning). Light devices (uiClass "Light")
25
+ // only expose setIntensity (0-100), no separate on/off.
26
+ //
27
+ // Known overkiz-client (1.0.23) quirk: Client.execute() stores the plain
28
+ // {label, actions} object we pass it into its internal executionPool, but
29
+ // fetchEvents() later assumes pooled entries are Execution instances and
30
+ // calls execution.onStateUpdate(), which throws for our commands ("Polling
31
+ // error - TypeError: execution.onStateUpdate is not a function"). That's
32
+ // caught inside the library and only affects completion-tracking of our own
33
+ // outgoing commands (externally-triggered state changes, e.g. via the Somfy
34
+ // app or remote, aren't affected and still push through normally) -- but to
35
+ // get a reliable resulting state for OUR commands we explicitly refresh the
36
+ // device afterwards instead of trusting that push.
16
37
  //
17
38
  // Johan Coppieters, 2026.
18
39
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
@@ -31,10 +52,15 @@ const overkiz_client_1 = require("overkiz-client");
31
52
  const types_1 = require("../duotecno/types");
32
53
  const logger_1 = require("../duotecno/logger");
33
54
  const base_1 = require("./base");
34
- // core:ClosureState / core:ClosureOrRollerShutterPositionState: 0 = fully
35
- // open, 100 = fully closed (opposite of HomeKit's TargetPosition scale).
55
+ // Position state: 0 = fully open/undeployed, 100 = fully closed/deployed
56
+ // (opposite of HomeKit's TargetPosition scale). Screens report
57
+ // core:ClosureState, Awnings report core:DeploymentState instead.
58
+ const kPositionStateNames = ["core:ClosureState", "core:DeploymentState"];
59
+ const kIntensityStateNames = ["core:LightIntensityState", "core:IntensityState"];
36
60
  const kOpenThreshold = 5;
37
61
  const kClosedThreshold = 95;
62
+ const kAwningUiClass = "Awning";
63
+ const kIgnoredUiClasses = ["ProtocolGateway"]; // bridges/gateways: not a controllable end-device
38
64
  function closureToUnitState(closure, previousClosure) {
39
65
  if (closure <= kOpenThreshold)
40
66
  return types_1.UnitState.kOpen;
@@ -62,11 +88,25 @@ class Overkiz extends base_1.Base {
62
88
  (0, logger_1.log)("overkiz", "Overkiz not configured (missing user/password) -> not starting.");
63
89
  }
64
90
  }
91
+ // (Re)connect using the current config -- used at startup and after the
92
+ // settings page saves new credentials, so a full process restart isn't
93
+ // required to pick them up.
65
94
  connect() {
66
95
  return __awaiter(this, void 0, void 0, function* () {
67
96
  const bridge = {
68
97
  warn: (...args) => (0, logger_1.log)("overkiz", args.join(" ")),
69
- error: (...args) => (0, logger_1.err)("overkiz", args.join(" ")),
98
+ error: (...args) => {
99
+ const msg = args.join(" ");
100
+ if (msg.includes("onStateUpdate is not a function")) {
101
+ // the known overkiz-client executionPool quirk described up top --
102
+ // harmless (the command still executes fine), just log it quietly
103
+ // instead of as an error.
104
+ (0, logger_1.debug)("overkiz", msg);
105
+ }
106
+ else {
107
+ (0, logger_1.err)("overkiz", msg);
108
+ }
109
+ },
70
110
  debug: (...args) => (0, logger_1.debug)("overkiz", args.join(" ")),
71
111
  info: (...args) => (0, logger_1.log)("overkiz", args.join(" ")),
72
112
  log: (...args) => (0, logger_1.log)("overkiz", args.join(" ")),
@@ -79,32 +119,46 @@ class Overkiz extends base_1.Base {
79
119
  refreshPeriod: this.config.refreshPeriod || 60,
80
120
  });
81
121
  const setup = yield this.client.getSetup();
122
+ this.devices.clear();
123
+ this.closures.clear();
82
124
  setup.devices.forEach(device => this.attachDevice(device));
83
125
  (0, logger_1.log)("overkiz", `connected -> ${setup.devices.length} device(s) found`);
84
126
  });
85
127
  }
86
128
  attachDevice(device) {
87
129
  this.devices.set(device.deviceURL, device);
88
- const closure = device.getNumber("core:ClosureState");
89
- if (device.hasState("core:ClosureState"))
90
- this.closures.set(device.deviceURL, closure);
130
+ const stateName = kPositionStateNames.find(n => device.hasState(n));
131
+ if (stateName)
132
+ this.closures.set(device.deviceURL, device.getNumber(stateName));
91
133
  device.on("states", (states) => this.onStates(device, states));
92
134
  }
93
135
  onStates(device, states) {
94
136
  (0, logger_1.debug)("overkiz", `states for ${device.label} -> ${JSON.stringify(states)}`);
95
- const closureState = states.find(s => s.name === "core:ClosureState");
96
- if (closureState) {
97
- const closure = Number(closureState.value);
137
+ const positionState = states.find(s => kPositionStateNames.includes(s.name));
138
+ if (positionState) {
139
+ const closure = Number(positionState.value);
98
140
  const previous = this.closures.get(device.deviceURL);
99
141
  const unitState = closureToUnitState(closure, previous);
100
142
  this.closures.set(device.deviceURL, closure);
101
- (0, logger_1.log)("overkiz", `${device.label} -> closure ${closure} -> UnitState ${types_1.UnitState[unitState]}`);
143
+ (0, logger_1.log)("overkiz", `${device.label} -> position ${closure} -> UnitState ${types_1.UnitState[unitState]}`);
102
144
  this.emitter.emit("state", device.deviceURL, unitState, closure);
103
145
  }
146
+ const intensityState = states.find(s => kIntensityStateNames.includes(s.name));
147
+ if (intensityState) {
148
+ const intensity = Number(intensityState.value);
149
+ (0, logger_1.log)("overkiz", `${device.label} -> intensity ${intensity}`);
150
+ this.emitter.emit("intensity", device.deviceURL, intensity);
151
+ }
152
+ }
153
+ uiClass(deviceURL) {
154
+ var _a;
155
+ return ((_a = this.devices.get(deviceURL)) === null || _a === void 0 ? void 0 : _a.definition.uiClass) || "";
104
156
  }
105
157
  listDevices() {
106
- return Array.from(this.devices.values()).map(d => ({
107
- deviceURL: d.deviceURL, label: d.label, widget: d.definition.widgetName,
158
+ return Array.from(this.devices.values())
159
+ .filter(d => !kIgnoredUiClasses.includes(d.definition.uiClass))
160
+ .map(d => ({
161
+ deviceURL: d.deviceURL, label: d.label, widget: d.definition.widgetName, uiClass: d.definition.uiClass,
108
162
  }));
109
163
  }
110
164
  execute(deviceURL, commandName, parameters = []) {
@@ -116,26 +170,46 @@ class Overkiz extends base_1.Base {
116
170
  label: "smartsocket",
117
171
  actions: [{ deviceURL, commands: [{ name: commandName, parameters }] }],
118
172
  });
173
+ // work around the executionPool/onStateUpdate quirk described above: get
174
+ // an authoritative resulting state ourselves rather than trusting the
175
+ // (possibly swallowed) push event for our own command.
176
+ this.client.refreshDeviceStates(deviceURL).catch(e => (0, logger_1.debug)("overkiz", `refreshDeviceStates failed: ${e.message || e}`));
119
177
  });
120
178
  }
179
+ hasCommand(deviceURL, commandName) {
180
+ var _a;
181
+ return ((_a = this.devices.get(deviceURL)) === null || _a === void 0 ? void 0 : _a.hasCommand(commandName)) || false;
182
+ }
121
183
  up(deviceURL) {
122
184
  return __awaiter(this, void 0, void 0, function* () {
123
- yield this.execute(deviceURL, "open");
185
+ yield this.execute(deviceURL, (this.uiClass(deviceURL) === kAwningUiClass) ? "undeploy" : "open");
124
186
  });
125
187
  }
126
188
  down(deviceURL) {
127
189
  return __awaiter(this, void 0, void 0, function* () {
128
- yield this.execute(deviceURL, "close");
190
+ yield this.execute(deviceURL, (this.uiClass(deviceURL) === kAwningUiClass) ? "deploy" : "close");
129
191
  });
130
192
  }
193
+ // Only send a real 'stop' -- never fake one (that's the whole point of this
194
+ // native integration over the Homebridge-accessory link path). If this
195
+ // particular device doesn't expose a 'stop' command, do nothing.
131
196
  stop(deviceURL) {
132
197
  return __awaiter(this, void 0, void 0, function* () {
198
+ if (!this.hasCommand(deviceURL, "stop")) {
199
+ (0, logger_1.log)("overkiz", `device ${deviceURL} has no native 'stop' command -> not sending anything`);
200
+ return;
201
+ }
133
202
  yield this.execute(deviceURL, "stop");
134
203
  });
135
204
  }
136
205
  setClosure(deviceURL, percentClosed) {
137
206
  return __awaiter(this, void 0, void 0, function* () {
138
- yield this.execute(deviceURL, "setClosure", [percentClosed]);
207
+ yield this.execute(deviceURL, (this.uiClass(deviceURL) === kAwningUiClass) ? "setDeployment" : "setClosure", [percentClosed]);
208
+ });
209
+ }
210
+ setIntensity(deviceURL, percent) {
211
+ return __awaiter(this, void 0, void 0, function* () {
212
+ yield this.execute(deviceURL, "setIntensity", [percent]);
139
213
  });
140
214
  }
141
215
  }
@@ -25,7 +25,9 @@ const logger_1 = require("../duotecno/logger");
25
25
  const protocol_1 = require("../duotecno/protocol");
26
26
  const somfy = require("./somfy");
27
27
  const fs_1 = require("fs");
28
+ const overkiz_1 = require("./overkiz");
28
29
  const config_1 = require("../duotecno/config");
30
+ const udpConfig_1 = require("../duotecno/udpConfig");
29
31
  const HB_1 = require("./HB");
30
32
  const HA_API_1 = require("./HA-API");
31
33
  const proxy_1 = require("./proxy");
@@ -73,7 +75,7 @@ class SmartApp extends webapp_1.WebApp {
73
75
  // get some configurated params
74
76
  const configuredPort = this.config.port || 80;
75
77
  this.password = platform.config.password || this.config.password || "";
76
- this.user = platform.config.user || this.config.user || "pi";
78
+ this.user = platform.config.user || this.config.user || "duotecno";
77
79
  // Set up multi-port listening if port is not 80
78
80
  // Note: Port 80 requires root/sudo on Unix-like systems
79
81
  // For development on macOS/Linux without sudo, use only configured port
@@ -516,6 +518,7 @@ function SmartSocketInit(message){
516
518
  // Settings //
517
519
  //////////////
518
520
  doSettings(context) {
521
+ var _a;
519
522
  return __awaiter(this, void 0, void 0, function* () {
520
523
  let config = this.read("settings");
521
524
  let message = "";
@@ -528,6 +531,9 @@ function SmartSocketInit(message){
528
531
  // Note: Full restart recommended for mDNS changes to take effect properly
529
532
  (0, logger_1.log)("smartapp", "Settings saved. mDNS name updated to: " + config.mdnsName);
530
533
  }
534
+ const overkizMessage = yield this.saveOverkizSettings(context);
535
+ if (overkizMessage)
536
+ message += " " + overkizMessage;
531
537
  }
532
538
  else if (context.action === "restart") {
533
539
  context.request = "restart";
@@ -536,7 +542,9 @@ function SmartSocketInit(message){
536
542
  else {
537
543
  // Just display the settings form
538
544
  }
539
- return this.ejs("settings", context, { config, message });
545
+ const overkizConfig = ((_a = this.power.overkiz) === null || _a === void 0 ? void 0 : _a.config) || types_1.Sanitizers.overkiz(null);
546
+ const overkizDevices = this.power.overkiz ? this.power.overkiz.listDevices() : [];
547
+ return this.ejs("settings", context, { config, message, overkizConfig, overkizDevices });
540
548
  });
541
549
  }
542
550
  scrapeSettings(context) {
@@ -683,6 +691,7 @@ function SmartSocketInit(message){
683
691
  (0, logger_1.log)("smartapp", "** error ** missing unit: " + (0, types_1.hex)(link.logicalNodeAddress) + "/" + (0, types_1.hex)(link.logicalAddress) + " **");
684
692
  }
685
693
  });
694
+ this.attachOverkizListeners();
686
695
  (0, HB_1.hbSubscribe)(() => __awaiter(this, void 0, void 0, function* () {
687
696
  // Get all values for subscribed accessories (links)
688
697
  if (this.links.length && (yield this.checkToken())) {
@@ -724,42 +733,49 @@ function SmartSocketInit(message){
724
733
  (0, logger_1.debug)("smartapp", "Received DT value from unit: " + u.getDisplayName() + " -> " + u.value + " / " + u.status + " from unit");
725
734
  if (!this.links.length)
726
735
  return;
727
- if (!(yield this.checkToken()))
728
- return;
736
+ // only bother logging into the Homebridge API if we actually have an accessory-type link to serve
737
+ const needsToken = this.links.some(link => link.type === types_1.LinkType.kAccessory);
738
+ const tokenOK = needsToken ? yield this.checkToken() : true;
729
739
  for (const link of this.links) {
730
- if (u.isUnit(link.masterAddress, link.masterPort, link.logicalNodeAddress, link.logicalAddress)) {
731
- const hbValue = (0, HB_1.dt2hbvalue)(u, link.hbValue, link.min, link.max);
732
- if (typeof hbValue != "undefined") {
733
- try {
734
- if (hbValue === -1) {
735
- if (link.nostop === "Y") {
736
- (0, logger_1.debug)("smartapp", "-> Not sending simulated 'stop' for accessory: " + link.accName + " of Duotecno unit: " + u.getDisplayName() + " (disabled)");
737
- }
738
- else {
739
- (0, logger_1.log)("smartapp", "-> Virtual unit send 'stop' for accessory: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
740
- const val = yield (0, HB_1.hbStop)(this.config.apiHost + ":" + this.config.apiPort, link.accId, link.accService, this.token);
741
- if (typeof val != "undefined")
742
- link.hbValue = val;
743
- }
744
- }
745
- else if (hbValue !== link.hbValue) {
746
- (0, logger_1.log)("smartapp", "-> Setting HB value for accessory: " + link.accName + " to " + hbValue + " of Duotecno unit: " + u.getDisplayName() + " with value " + u.value);
747
- link.hbValue = hbValue;
748
- yield (0, HB_1.hbSetState)(this.config.apiHost + ":" + this.config.apiPort, link.accId, link.accService, hbValue, this.token);
740
+ if (!u.isUnit(link.masterAddress, link.masterPort, link.logicalNodeAddress, link.logicalAddress))
741
+ continue;
742
+ if (link.type === types_1.LinkType.kOverkiz) {
743
+ yield this.informOverkizLink(u, link);
744
+ continue;
745
+ }
746
+ if (!tokenOK)
747
+ continue;
748
+ const hbValue = (0, HB_1.dt2hbvalue)(u, link.hbValue, link.min, link.max);
749
+ if (typeof hbValue != "undefined") {
750
+ try {
751
+ if (hbValue === -1) {
752
+ if (link.nostop === "Y") {
753
+ (0, logger_1.debug)("smartapp", "-> Not sending simulated 'stop' for accessory: " + link.accName + " of Duotecno unit: " + u.getDisplayName() + " (disabled)");
749
754
  }
750
755
  else {
751
- (0, logger_1.debug)("smartapp", "-> No change for accessory: " + link.accName + " to " + u.value + " / " + u.status);
756
+ (0, logger_1.log)("smartapp", "-> Virtual unit send 'stop' for accessory: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
757
+ const val = yield (0, HB_1.hbStop)(this.config.apiHost + ":" + this.config.apiPort, link.accId, link.accService, this.token);
758
+ if (typeof val != "undefined")
759
+ link.hbValue = val;
752
760
  }
753
761
  }
754
- catch (e) {
755
- if (e instanceof HB_1.HbAuthError) {
756
- (0, logger_1.err)("smartapp", "informLinks: Homebridge API token expired for accessory " + link.accName + " -> forcing re-login on next update");
757
- this.token = "";
758
- (0, config_1.setToken)("");
759
- }
760
- else {
761
- (0, logger_1.err)("smartapp", "informLinks: failed to set HB value for accessory " + link.accName + " -> " + e.toString());
762
- }
762
+ else if (hbValue !== link.hbValue) {
763
+ (0, logger_1.log)("smartapp", "-> Setting HB value for accessory: " + link.accName + " to " + hbValue + " of Duotecno unit: " + u.getDisplayName() + " with value " + u.value);
764
+ link.hbValue = hbValue;
765
+ yield (0, HB_1.hbSetState)(this.config.apiHost + ":" + this.config.apiPort, link.accId, link.accService, hbValue, this.token);
766
+ }
767
+ else {
768
+ (0, logger_1.debug)("smartapp", "-> No change for accessory: " + link.accName + " to " + u.value + " / " + u.status);
769
+ }
770
+ }
771
+ catch (e) {
772
+ if (e instanceof HB_1.HbAuthError) {
773
+ (0, logger_1.err)("smartapp", "informLinks: Homebridge API token expired for accessory " + link.accName + " -> forcing re-login on next update");
774
+ this.token = "";
775
+ (0, config_1.setToken)("");
776
+ }
777
+ else {
778
+ (0, logger_1.err)("smartapp", "informLinks: failed to set HB value for accessory " + link.accName + " -> " + e.toString());
763
779
  }
764
780
  }
765
781
  }
@@ -767,6 +783,243 @@ function SmartSocketInit(message){
767
783
  ;
768
784
  });
769
785
  }
786
+ // Duotecno -> Overkiz direction of a kOverkiz link. Mirrors the kAccessory
787
+ // branch above, but talks straight to the Overkiz device instead of going
788
+ // through the Homebridge REST API -- and unlike hbStop(), overkiz.stop()
789
+ // is a genuine 'stop' command, not a simulated one.
790
+ informOverkizLink(u, link) {
791
+ return __awaiter(this, void 0, void 0, function* () {
792
+ if (!this.power.overkiz)
793
+ return;
794
+ if (u.isUpDown()) {
795
+ yield this.informOverkizUpDown(u, link);
796
+ return;
797
+ }
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).
814
+ const hbValue = (0, HB_1.dt2hbvalue)(u, link.hbValue, link.min, link.max);
815
+ if ((typeof hbValue === "undefined") || (hbValue === link.hbValue))
816
+ return;
817
+ try {
818
+ link.hbValue = hbValue;
819
+ (0, logger_1.log)("smartapp", "-> Setting Overkiz intensity for: " + link.accName + " to " + hbValue + " of Duotecno unit: " + u.getDisplayName());
820
+ yield this.power.overkiz.setIntensity(link.accId, hbValue);
821
+ }
822
+ catch (e) {
823
+ (0, logger_1.err)("smartapp", "informOverkizLink: failed for " + link.accName + " -> " + e.toString());
824
+ }
825
+ });
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
+ }
853
+ // A Duotecno up/down (kSwitchingMotor) unit reports status two different
854
+ // ways depending on how it's wired in the Duotecno programming:
855
+ // - a "virtual button" duoswitch macro event -> unit.value = 10 (stop) /
856
+ // 11 (up) / 12 (down), a one-shot pulse (this is what dt2hbvalue/HB.ts
857
+ // expects, and is what the existing Homebridge-accessory links rely on).
858
+ // - the real motor's own busy/idle status -> unit.value === unit.status,
859
+ // one of 0 (stopped), 1 (stopped/down), 2 (stopped/up), 3 (busy/down),
860
+ // 4 (busy/up) -- e.g. the "Garage" unit, which has no duoswitch macro
861
+ // wired to it and only ever reports these real values.
862
+ // Handle both: a real motor's busy<->idle transition is translated into
863
+ // up/down/stop the same way a virtual pulse would be.
864
+ informOverkizUpDown(u, link) {
865
+ return __awaiter(this, void 0, void 0, function* () {
866
+ if (!this.power.overkiz)
867
+ return;
868
+ const rawValue = Math.round(u.value);
869
+ try {
870
+ if ((rawValue === 10) || (rawValue === 11) || (rawValue === 12)) {
871
+ // virtual duoswitch pulse
872
+ if (rawValue === link.hbValue)
873
+ return;
874
+ link.hbValue = rawValue;
875
+ if (rawValue === 10) {
876
+ if (link.nostop === "Y") {
877
+ (0, logger_1.debug)("smartapp", "-> Not sending 'stop' to Overkiz device: " + link.accName + " (disabled)");
878
+ }
879
+ else {
880
+ (0, logger_1.log)("smartapp", "-> Sending 'stop' to Overkiz device: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
881
+ yield this.power.overkiz.stop(link.accId);
882
+ }
883
+ }
884
+ else if (rawValue === 11) {
885
+ (0, logger_1.log)("smartapp", "-> Sending up/open to Overkiz device: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
886
+ yield this.power.overkiz.up(link.accId);
887
+ }
888
+ else {
889
+ (0, logger_1.log)("smartapp", "-> Sending down/close to Overkiz device: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
890
+ yield this.power.overkiz.down(link.accId);
891
+ }
892
+ return;
893
+ }
894
+ // real motor busy/idle status
895
+ const status = u.status;
896
+ const wasBusy = (link.hbValue === 3) || (link.hbValue === 4);
897
+ const isBusy = (status === 3) || (status === 4);
898
+ if (isBusy && (status !== link.hbValue)) {
899
+ link.hbValue = status;
900
+ if (status === 4) {
901
+ (0, logger_1.log)("smartapp", "-> Sending up/open to Overkiz device: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
902
+ yield this.power.overkiz.up(link.accId);
903
+ }
904
+ else {
905
+ (0, logger_1.log)("smartapp", "-> Sending down/close to Overkiz device: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
906
+ yield this.power.overkiz.down(link.accId);
907
+ }
908
+ }
909
+ else if (!isBusy && wasBusy) {
910
+ link.hbValue = status;
911
+ if (link.nostop === "Y") {
912
+ (0, logger_1.debug)("smartapp", "-> Not sending 'stop' to Overkiz device: " + link.accName + " (disabled)");
913
+ }
914
+ else {
915
+ (0, logger_1.log)("smartapp", "-> Sending 'stop' to Overkiz device: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
916
+ yield this.power.overkiz.stop(link.accId);
917
+ }
918
+ }
919
+ else {
920
+ link.hbValue = status;
921
+ }
922
+ }
923
+ catch (e) {
924
+ (0, logger_1.err)("smartapp", "informOverkizUpDown: failed for " + link.accName + " -> " + e.toString());
925
+ }
926
+ });
927
+ }
928
+ attachOverkizListeners() {
929
+ if (!this.power.overkiz || (this.power.overkiz === this.attachedOverkiz))
930
+ return;
931
+ this.attachedOverkiz = this.power.overkiz;
932
+ this.power.overkiz.emitter.on("state", (deviceURL, unitState, closure) => {
933
+ for (const link of this.links) {
934
+ if ((link.type !== types_1.LinkType.kOverkiz) || (link.accId !== deviceURL) || !link.unit)
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;
953
+ if (unitState === link.dtValue)
954
+ continue; // no repeat
955
+ link.dtValue = unitState;
956
+ if ((unitState === types_1.UnitState.kOpen) || (unitState === types_1.UnitState.kOpening)) {
957
+ (0, logger_1.log)("smartapp", "-> Overkiz reports " + types_1.UnitState[unitState] + " for: " + link.accName + " -> setting Duotecno unit " + link.unit.getDisplayName() + " to open");
958
+ link.unit.setState(types_1.UnitMotorCmd.kOpen);
959
+ }
960
+ else if ((unitState === types_1.UnitState.kClosed) || (unitState === types_1.UnitState.kClosing)) {
961
+ (0, logger_1.log)("smartapp", "-> Overkiz reports " + types_1.UnitState[unitState] + " for: " + link.accName + " -> setting Duotecno unit " + link.unit.getDisplayName() + " to close");
962
+ link.unit.setState(types_1.UnitMotorCmd.kClose);
963
+ }
964
+ else {
965
+ // kStopped: Overkiz reports the device sitting still somewhere in
966
+ // the middle (e.g. stopped mid-way via the Somfy app/remote) --
967
+ // forward a genuine stop, same as the Duotecno -> Overkiz direction.
968
+ if (link.nostop === "Y") {
969
+ (0, logger_1.debug)("smartapp", "-> Not forwarding Overkiz 'stopped' for: " + link.accName + " (disabled)");
970
+ }
971
+ else {
972
+ (0, logger_1.log)("smartapp", "-> Overkiz reports stopped for: " + link.accName + " -> setting Duotecno unit " + link.unit.getDisplayName() + " to stop");
973
+ link.unit.setState(types_1.UnitMotorCmd.kStop);
974
+ }
975
+ }
976
+ }
977
+ });
978
+ this.power.overkiz.emitter.on("intensity", (deviceURL, intensity) => {
979
+ for (const link of this.links) {
980
+ if ((link.type !== types_1.LinkType.kOverkiz) || (link.accId !== deviceURL) || !link.unit)
981
+ continue;
982
+ const { dtValue, dtTarget } = (0, HB_1.hb2dtvalue)(link.unit, intensity);
983
+ if (dtValue !== link.dtValue) {
984
+ (0, logger_1.log)("smartapp", "-> Setting value of Duotecno unit: " + link.unit.getDisplayName() + " to " + dtTarget + " from Overkiz intensity " + intensity + " of: " + link.accName);
985
+ link.dtValue = dtValue;
986
+ link.unit.setState(dtTarget);
987
+ }
988
+ }
989
+ });
990
+ }
991
+ //////////////////////////////
992
+ // Overkiz (Somfy/TaHoma) //
993
+ //////////////////////////////
994
+ // Saves the Overkiz fields living on the Settings page and (re)connects.
995
+ // Only attempts a connection when credentials are actually present, so
996
+ // saving the rest of the Settings form doesn't produce a spurious
997
+ // "connection failed" for installs that don't use Overkiz at all.
998
+ saveOverkizSettings(context) {
999
+ var _a, _b;
1000
+ return __awaiter(this, void 0, void 0, function* () {
1001
+ const service = context.getParam({ name: "service", type: "string", default: "local" });
1002
+ const user = context.getParam({ name: "user", type: "string", default: "" });
1003
+ const submittedPassword = context.getParam({ name: "password", type: "string", default: "" });
1004
+ // blank password field means "keep the current one"
1005
+ const password = submittedPassword || ((_b = (_a = this.power.overkiz) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.password) || "";
1006
+ const pollingPeriod = context.getParam({ name: "pollingPeriod", type: "integer", default: 10 });
1007
+ if (!this.power.overkiz)
1008
+ this.power.overkiz = new overkiz_1.Overkiz(this.system);
1009
+ this.power.overkiz.config = types_1.Sanitizers.overkiz({ service, user, password, pollingPeriod });
1010
+ this.power.overkiz.writeConfig();
1011
+ if (!user || !password)
1012
+ return "";
1013
+ try {
1014
+ yield this.power.overkiz.connect();
1015
+ this.attachOverkizListeners();
1016
+ return "Overkiz connected successfully -> " + this.power.overkiz.listDevices().length + " device(s) found.";
1017
+ }
1018
+ catch (e) {
1019
+ return "Overkiz connection failed: " + (e.message || e.toString());
1020
+ }
1021
+ });
1022
+ }
770
1023
  checkToken() {
771
1024
  return __awaiter(this, void 0, void 0, function* () {
772
1025
  if (!this.token) {
@@ -793,6 +1046,7 @@ function SmartSocketInit(message){
793
1046
  return __awaiter(this, void 0, void 0, function* () {
794
1047
  let inx = parseInt(context.id);
795
1048
  const accessories = yield this.getAccessories();
1049
+ const overkizDevices = this.power.overkiz ? this.power.overkiz.listDevices() : [];
796
1050
  const units = this.system.allUsedUnits();
797
1051
  let message;
798
1052
  try {
@@ -800,11 +1054,11 @@ function SmartSocketInit(message){
800
1054
  this.links.push(Object.assign(Object.assign({}, types_1.kEmptyLink), { type: types_1.LinkType.kAccessory }));
801
1055
  inx = this.links.length - 1;
802
1056
  return this.ejs("linkDetail", context, { link: this.links[inx], id: inx,
803
- masters: this.system.masters, accessories, units });
1057
+ masters: this.system.masters, accessories, overkizDevices, units });
804
1058
  }
805
1059
  else if (context.action === "edit") {
806
1060
  return this.ejs("linkDetail", context, { link: this.links[inx], id: inx,
807
- masters: this.system.masters, accessories, units });
1061
+ masters: this.system.masters, accessories, overkizDevices, units });
808
1062
  }
809
1063
  else if (context.action === "delete") {
810
1064
  this.deleteLink(inx);
@@ -824,22 +1078,29 @@ function SmartSocketInit(message){
824
1078
  });
825
1079
  }
826
1080
  scrapeLink(context) {
1081
+ var _a;
827
1082
  // don't take the name of the unit
828
1083
  const { masterAddress, masterPort, logicalAddress, logicalNodeAddress } = this.scrapeUnit(context, '');
829
- // for now only accessory links.
830
- const stype = context.getParam({ name: "type", type: "string", default: types_1.LinkType.kAccessory });
831
1084
  const name = context.getParam({ name: "name", type: "string", default: "--" });
832
1085
  // max & min values
833
1086
  const min = context.getParam({ name: "min", type: "integer", default: 0 });
834
1087
  const max = context.getParam({ name: "max", type: "integer", default: 100 });
835
1088
  // don't forward the simulated 'stop' of an up/down link to the accessory
836
1089
  const nostop = context.getParam({ name: "nostop", type: "string", default: "N" });
837
- // uniqiue id for the accessory
838
- const accId = context.getParam({ name: "accId", type: "string", default: "" });
839
- const accName = (0, HB_1.hbName)(accId);
840
- const accService = (0, HB_1.hbService)(accId);
841
- return { accId, accName, accService, name, masterAddress, masterPort, logicalAddress, logicalNodeAddress, type: stype, min, max, nostop };
842
- ;
1090
+ // the accessory dropdown offers both Homebridge accessories and Overkiz
1091
+ // devices in one list; an Overkiz option's value is prefixed so we know
1092
+ // which provider -- and hence Link type -- the picked id belongs to.
1093
+ const rawAccId = context.getParam({ name: "accId", type: "string", default: "" });
1094
+ const kOverkizPrefix = "overkiz:";
1095
+ if (rawAccId.startsWith(kOverkizPrefix)) {
1096
+ const accId = rawAccId.substring(kOverkizPrefix.length);
1097
+ const device = (_a = this.power.overkiz) === null || _a === void 0 ? void 0 : _a.listDevices().find(d => d.deviceURL === accId);
1098
+ return { accId, accName: (device === null || device === void 0 ? void 0 : device.label) || "", accService: device ? [device.uiClass] : [],
1099
+ name, masterAddress, masterPort, logicalAddress, logicalNodeAddress, type: types_1.LinkType.kOverkiz, min, max, nostop };
1100
+ }
1101
+ const accName = (0, HB_1.hbName)(rawAccId);
1102
+ const accService = (0, HB_1.hbService)(rawAccId);
1103
+ return { accId: rawAccId, accName, accService, name, masterAddress, masterPort, logicalAddress, logicalNodeAddress, type: types_1.LinkType.kAccessory, min, max, nostop };
843
1104
  }
844
1105
  updateLink(inx, link) {
845
1106
  if ((inx >= 0) && (inx < this.links.length)) {
@@ -1471,7 +1732,7 @@ function SmartSocketInit(message){
1471
1732
  let message;
1472
1733
  try {
1473
1734
  if (context.action === "new") {
1474
- 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 });
1475
1736
  }
1476
1737
  else if (context.action === "list") {
1477
1738
  return this.ejs("unitList", context, { masters: this.system.masters });
@@ -1483,7 +1744,7 @@ function SmartSocketInit(message){
1483
1744
  context.getMaster("id");
1484
1745
  const master = this.system.findMaster(context["masterAddress"], context["masterPort"]);
1485
1746
  if (master)
1486
- 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 });
1487
1748
  else
1488
1749
  message = "Error: Master not found";
1489
1750
  }
@@ -1510,6 +1771,27 @@ function SmartSocketInit(message){
1510
1771
  else
1511
1772
  message = "Error: Master not found";
1512
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
+ }
1513
1795
  }
1514
1796
  catch (e) {
1515
1797
  message = e.toString();
@@ -61,16 +61,24 @@
61
61
  <select name="accId" id="accId">
62
62
  <option value="" <%= (link.accId == '') ? 'selected' : '' %>>Select an Accessory</option>
63
63
  <% for (let a of accessories) { %>
64
- <% if (a.manufacturer != manufacturer) {
65
- if (manufacturer) { %> </optgroup> <% }
66
- manufacturer = a.manufacturer; %>
64
+ <% const accManufacturer = (a.manufacturer.indexOf("Coppieters") !== -1) ? "Duotecno" : a.manufacturer; %>
65
+ <% if (accManufacturer != manufacturer) {
66
+ if (manufacturer) { %> </optgroup> <% }
67
+ manufacturer = accManufacturer; %>
67
68
  <optgroup label="<%= manufacturer %>">
68
69
  <% } %>
69
- <option value="<%= a.id %>" <%= (link.accId == a.id) ? 'selected' : '' %>><%= a.name %></option>
70
- <% }
70
+ <option value="<%= a.id %>" <%= ((link.type == 'accessory') && (link.accId == a.id)) ? 'selected' : '' %>><%= a.name %></option>
71
+ <% }
71
72
  if (manufacturer) { %> </optgroup> <% } %>
73
+ <% if (overkizDevices && overkizDevices.length) { %>
74
+ <optgroup label="Overkiz">
75
+ <% for (let d of overkizDevices) { %>
76
+ <option value="overkiz:<%= d.deviceURL %>" <%= ((link.type == 'overkiz') && (link.accId == d.deviceURL)) ? 'selected' : '' %>><%= d.label %> (<%= d.uiClass %>)</option>
77
+ <% } %>
78
+ </optgroup>
79
+ <% } %>
72
80
  </select>
73
- <label for="id">Linked HB accessory</label>
81
+ <label for="id">Linked accessory (Homebridge or Overkiz)</label>
74
82
  </div>
75
83
  </div>
76
84
 
@@ -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
@@ -16,17 +16,25 @@
16
16
  <li <% if (request === "masters") { %>class="active"<% } %>><a href="<%= basePath %>/masters">Masters</a></li>
17
17
  <li <% if (request === "services") { %>class="active"<% } %>><a href="<%= basePath %>/services">Services</a></li>
18
18
  <li <% if (request === "proxy") { %>class="active"<% } %>><a href="<%= basePath %>/proxy">Proxy</a></li>
19
- <li <% if (request === "links") { %>class="active"<% } %>><a href="<%= basePath %>/links">Links</a></li>
20
- <li <% if (request === "devices") { %>class="active"<% } %>><a href="<%= basePath %>/devices">Devices</a></li>
21
- <% if (hasSmappee) { %><li <% if ((request === "power") && (type === "smappee")) { %>class="active"<% } %>><a href="<%= basePath %>/power?type=smappee">Smappee</a></li><% } %>
22
- <% if (hasP1) { %><li <% if ((request === "power") && (type === "p1")) { %>class="active"<% } %>><a href="<%= basePath %>/power?type=p1">P1</a></li><% } %>
23
- <% if (hasShelly) { %><li <% if ((request === "power") && (type === "shelly")) { %>class="active"<% } %>><a href="<%= basePath %>/power?type=shelly">Shelly</a></li><% } %>
24
- <li <% if (request === "switches") { %>class="active"<% } %>><a href="<%= basePath %>/switches">Switches</a></li>
19
+ <li <% if ((request === "links") || (request === "switches")) { %>class="active"<% } %>><a class="dropdown-trigger" href="#" data-target="linking-dropdown">Linking<i class="material-icons right">arrow_drop_down</i></a></li>
20
+ <li <% if ((request === "devices") || (request === "power")) { %>class="active"<% } %>><a class="dropdown-trigger" href="#" data-target="energy-dropdown">Energy<i class="material-icons right">arrow_drop_down</i></a></li>
25
21
  <li <% if (request === "settings") { %>class="active"<% } %>><a href="<%= basePath %>/settings">Settings</a></li>
26
22
  </ul>
27
23
  </div>
28
24
  </nav>
29
25
 
26
+ <ul id="linking-dropdown" class="dropdown-content">
27
+ <li <% if (request === "switches") { %>class="active"<% } %>><a href="<%= basePath %>/switches">Http Switches</a></li>
28
+ <li <% if (request === "links") { %>class="active"<% } %>><a href="<%= basePath %>/links">Accessory Links</a></li>
29
+ </ul>
30
+
31
+ <ul id="energy-dropdown" class="dropdown-content">
32
+ <li <% if (request === "devices") { %>class="active"<% } %>><a href="<%= basePath %>/devices">Websocket devices</a></li>
33
+ <% if (hasSmappee) { %><li <% if ((request === "power") && (type === "smappee")) { %>class="active"<% } %>><a href="<%= basePath %>/power?type=smappee">Smappee</a></li><% } %>
34
+ <% if (hasP1) { %><li <% if ((request === "power") && (type === "p1")) { %>class="active"<% } %>><a href="<%= basePath %>/power?type=p1">P1</a></li><% } %>
35
+ <% if (hasShelly) { %><li <% if ((request === "power") && (type === "shelly")) { %>class="active"<% } %>><a href="<%= basePath %>/power?type=shelly">Shelly</a></li><% } %>
36
+ </ul>
37
+
30
38
  <!-- Mobile Slide-out Menu -->
31
39
  <ul class="sidenav" id="mobile-menu">
32
40
  <li><div class="user-view">
@@ -35,16 +43,18 @@
35
43
  </div></li>
36
44
  <li <% if (request === "masters") { %>class="active"<% } %>><a href="<%= basePath %>/masters">Masters</a></li>
37
45
  <li <% if (request === "services") { %>class="active"<% } %>><a href="<%= basePath %>/services">Services</a></li>
46
+ <li <% if (request === "proxy") { %>class="active"<% } %>><a href="<%= basePath %>/proxy">Proxy</a></li>
38
47
  <li><div class="divider"></div></li>
39
- <li <% if (request === "devices") { %>class="active"<% } %>><a href="<%= basePath %>/devices">Devices</a></li>
40
- <li <% if (request === "links") { %>class="active"<% } %>><a href="<%= basePath %>/links">Links</a></li>
41
- <li <% if (request === "switches") { %>class="active"<% } %>><a href="<%= basePath %>/switches">Switches</a></li>
48
+ <li class="subheader">Linking</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>
42
51
  <li><div class="divider"></div></li>
52
+ <li class="subheader">Energy</li>
53
+ <li <% if (request === "devices") { %>class="active"<% } %>><a href="<%= basePath %>/devices">Julix</a></li>
43
54
  <% if (hasSmappee) { %><li <% if ((request === "power") && (type === "smappee")) { %>class="active"<% } %>><a href="<%= basePath %>/power?type=smappee">Smappee</a></li><% } %>
44
55
  <% if (hasP1) { %><li <% if ((request === "power") && (type === "p1")) { %>class="active"<% } %>><a href="<%= basePath %>/power?type=p1">P1 Meter</a></li><% } %>
45
56
  <% if (hasShelly) { %><li <% if ((request === "power") && (type === "shelly")) { %>class="active"<% } %>><a href="<%= basePath %>/power?type=shelly">Shelly PM</a></li><% } %>
46
57
  <li><div class="divider"></div></li>
47
- <li <% if (request === "proxy") { %>class="active"<% } %>><a href="<%= basePath %>/proxy">Proxy</a></li>
48
58
  <li <% if (request === "settings") { %>class="active"<% } %>><a href="<%= basePath %>/settings">Settings</a></li>
49
59
  </ul>
50
60
 
@@ -2,6 +2,7 @@
2
2
  <html>
3
3
  <head>
4
4
  <%- include('/views/head'); -%>
5
+ <style> .select-wrapper { max-width: 300px !important; } </style>
5
6
  </head>
6
7
 
7
8
  <body>
@@ -14,9 +15,9 @@
14
15
  <% if (typeof message !== 'undefined' && message) { %>
15
16
  <div class="row">
16
17
  <div class="col s12">
17
- <div class="card-panel <%= message.includes('successfully') ? 'green lighten-4' : 'orange lighten-4' %>">
18
- <span class="<%= message.includes('successfully') ? 'green-text text-darken-4' : 'orange-text text-darken-4' %>">
19
- <i class="material-icons left"><%= message.includes('successfully') ? 'check_circle' : 'info' %></i>
18
+ <div class="card-panel <%= message.includes('failed') ? 'orange lighten-4' : 'green lighten-4' %>">
19
+ <span class="<%= message.includes('failed') ? 'orange-text text-darken-4' : 'green-text text-darken-4' %>">
20
+ <i class="material-icons left"><%= message.includes('failed') ? 'info' : 'check_circle' %></i>
20
21
  <%= message %>
21
22
  </span>
22
23
  </div>
@@ -58,6 +59,71 @@
58
59
  </div>
59
60
  </div>
60
61
 
62
+ <style>
63
+ /* Materialize floats active labels to translateY(-14px), which sits too
64
+ close to the input value here -- push these up further. Scoped to this
65
+ section only, not a site-wide change. */
66
+ .overkiz-settings .input-field > label.active {
67
+ transform: translateY(-26px) scale(0.8) !important;
68
+ -webkit-transform: translateY(-26px) scale(0.8) !important;
69
+ }
70
+ </style>
71
+ <div class="overkiz-settings">
72
+ <h5>Overkiz (Somfy / TaHoma)</h5>
73
+ <div class="row">
74
+ <div class="input-field col s3">
75
+ <select name="service" id="service">
76
+ <option value="local" <%= (overkizConfig.service == 'local') ? 'selected' : '' %>>Local (developer mode)</option>
77
+ <option value="tahoma" <%= (overkizConfig.service == 'tahoma') ? 'selected' : '' %>>Cloud - TaHoma / Somfy Europe</option>
78
+ <option value="somfy_australia" <%= (overkizConfig.service == 'somfy_australia') ? 'selected' : '' %>>Cloud - Somfy Australia</option>
79
+ <option value="somfy_north_america" <%= (overkizConfig.service == 'somfy_north_america') ? 'selected' : '' %>>Cloud - Somfy North America</option>
80
+ <option value="connexoon" <%= (overkizConfig.service == 'connexoon') ? 'selected' : '' %>>Cloud - Connexoon</option>
81
+ <option value="cozytouch" <%= (overkizConfig.service == 'cozytouch') ? 'selected' : '' %>>Cloud - Cozytouch</option>
82
+ <option value="hi_kumo" <%= (overkizConfig.service == 'hi_kumo') ? 'selected' : '' %>>Cloud - Hi Kumo</option>
83
+ </select>
84
+ <label for="service">Service</label>
85
+ </div>
86
+
87
+ <div class="input-field col s3">
88
+ <input placeholder="1234-5678-9012 or IP" id="user" name="user" type="text" value="<%= overkizConfig.user %>">
89
+ <label for="user">User (local: gateway PIN or IP -- cloud: account username)</label>
90
+ </div>
91
+
92
+ <div class="input-field col s3">
93
+ <input placeholder="leave blank to keep current" id="password" name="password" type="password" value="">
94
+ <label for="password">Password (local: developer-mode token -- cloud: account password)</label>
95
+ </div>
96
+
97
+ <div class="input-field col s3">
98
+ <input placeholder="10" id="pollingPeriod" name="pollingPeriod" type="number" min="1" value="<%= overkizConfig.pollingPeriod %>">
99
+ <label for="pollingPeriod">Polling period (sec) -- how often we check for remote/app-triggered changes</label>
100
+ </div>
101
+ </div>
102
+
103
+ <div class="row">
104
+ <div class="col s12">
105
+ <b>Local developer mode</b>: activate it in the Somfy TaHoma app first
106
+ (<a href="https://developer.somfy.com/somfy-tahoma-developer-mode" target="_blank">developer.somfy.com</a>),
107
+ which gives you the gateway PIN and lets you generate a token.<br />
108
+ <b>Cloud mode</b> in Belgium choose: Cloud - TaHoma / Somfy Europe.<br />
109
+ Saving here (re)connects immediately.
110
+ </div>
111
+ </div>
112
+
113
+ <% if (overkizDevices && overkizDevices.length) { %>
114
+ <table>
115
+ <thead>
116
+ <tr><th>Label</th><th>Type</th><th>deviceURL</th></tr>
117
+ </thead>
118
+ <tbody>
119
+ <% for (let d of overkizDevices) { %>
120
+ <tr><td><%= d.label %></td><td><%= d.uiClass %> (<%= d.widget %>)</td><td><%= d.deviceURL %></td></tr>
121
+ <% } %>
122
+ </tbody>
123
+ </table>
124
+ <% } %>
125
+ </div>
126
+
61
127
  <div class="row">
62
128
  <div class="col s12">
63
129
  <button class="btn waves-effect waves-light" type="submit" name="action" value="save">