homebridge-smartsystem 6.8.0 → 6.8.4
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.
- package/README.md +4 -0
- package/config.json +27 -49
- package/duotecno/logger.js +1 -1
- package/duotecno/logger.ts +1 -1
- package/duotecno/protocol.js +10 -1
- package/duotecno/protocol.ts +10 -1
- package/duotecno/types.js +4 -2
- package/duotecno/types.ts +6 -2
- package/package.json +1 -1
- package/server/HB.js +52 -14
- package/server/HB.ts +50 -11
- package/server/proxy.js +60 -34
- package/server/proxy.ts +73 -40
- package/server/smartapp.js +22 -10
- package/server/smartapp.ts +24 -13
- package/server/views/link-details.ejs +12 -2
- package/server/webapp.js +10 -4
- package/server/webapp.ts +10 -4
package/README.md
CHANGED
|
@@ -301,8 +301,12 @@ Homebridge UI version
|
|
|
301
301
|
- 3: do dimmers/up-downs
|
|
302
302
|
- 4: better up-down values to and from HB
|
|
303
303
|
|
|
304
|
+
|
|
304
305
|
### v6.8.0 - 08/07/2025
|
|
305
306
|
- 0: first version including the proxy
|
|
307
|
+
- 1: max/min values for hbValues
|
|
308
|
+
- 2: HB up/down -> don't respond to motor unit's, only macro
|
|
309
|
+
- 3: remember request before the login kicked in (and redirect to that one, once the login was succesful)
|
|
306
310
|
|
|
307
311
|
## Hardware
|
|
308
312
|
A Raspberry Pi that connects to a Duotecno IP Node
|
package/config.json
CHANGED
|
@@ -194,42 +194,6 @@
|
|
|
194
194
|
"type": "httpdim",
|
|
195
195
|
"plug": "http://192.168.0.96/light/0?brightness=$&turn=#"
|
|
196
196
|
},
|
|
197
|
-
{
|
|
198
|
-
"logicalNodeAddress": 252,
|
|
199
|
-
"logicalAddress": 105,
|
|
200
|
-
"masterAddress": "192.168.0.97",
|
|
201
|
-
"masterPort": 5001,
|
|
202
|
-
"name": "Screen Salon Top",
|
|
203
|
-
"displayName": "Screen Salon Top",
|
|
204
|
-
"extendedType": 0,
|
|
205
|
-
"nomacro": "N",
|
|
206
|
-
"nostop": "N",
|
|
207
|
-
"unitName": "Salon Top",
|
|
208
|
-
"value": 0,
|
|
209
|
-
"data": "",
|
|
210
|
-
"header": "",
|
|
211
|
-
"method": "GET",
|
|
212
|
-
"type": "somfy",
|
|
213
|
-
"plug": "1"
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
"logicalNodeAddress": 252,
|
|
217
|
-
"logicalAddress": 104,
|
|
218
|
-
"masterAddress": "192.168.0.97",
|
|
219
|
-
"masterPort": 5001,
|
|
220
|
-
"name": "Screen Tim small",
|
|
221
|
-
"displayName": "Screen Tim small",
|
|
222
|
-
"extendedType": 0,
|
|
223
|
-
"nomacro": "N",
|
|
224
|
-
"nostop": "Y",
|
|
225
|
-
"unitName": "Tim Small",
|
|
226
|
-
"value": 0,
|
|
227
|
-
"data": "",
|
|
228
|
-
"header": "",
|
|
229
|
-
"method": "GET",
|
|
230
|
-
"type": "httpupdown",
|
|
231
|
-
"plug": "2"
|
|
232
|
-
},
|
|
233
197
|
{
|
|
234
198
|
"logicalNodeAddress": 16,
|
|
235
199
|
"logicalAddress": 35,
|
|
@@ -254,8 +218,8 @@
|
|
|
254
218
|
"password": "GMTecno",
|
|
255
219
|
"links": [
|
|
256
220
|
{
|
|
257
|
-
"logicalNodeAddress":
|
|
258
|
-
"logicalAddress":
|
|
221
|
+
"logicalNodeAddress": 252,
|
|
222
|
+
"logicalAddress": 104,
|
|
259
223
|
"masterAddress": "192.168.0.97",
|
|
260
224
|
"masterPort": 5001,
|
|
261
225
|
"name": "unit",
|
|
@@ -269,7 +233,9 @@
|
|
|
269
233
|
"TargetPosition",
|
|
270
234
|
"ObstructionDetected"
|
|
271
235
|
],
|
|
272
|
-
"type": "accessory"
|
|
236
|
+
"type": "accessory",
|
|
237
|
+
"max": 100,
|
|
238
|
+
"min": 0
|
|
273
239
|
}
|
|
274
240
|
]
|
|
275
241
|
},
|
|
@@ -304,7 +270,7 @@
|
|
|
304
270
|
"masterPort": 5001,
|
|
305
271
|
"name": "<Solar Panels",
|
|
306
272
|
"displayName": "<Solar Panels",
|
|
307
|
-
"extendedType":
|
|
273
|
+
"extendedType": 0,
|
|
308
274
|
"active": "N",
|
|
309
275
|
"used": "Y",
|
|
310
276
|
"group": 0
|
|
@@ -316,7 +282,7 @@
|
|
|
316
282
|
"masterPort": 5001,
|
|
317
283
|
"name": ">Cars",
|
|
318
284
|
"displayName": ">Cars",
|
|
319
|
-
"extendedType":
|
|
285
|
+
"extendedType": 0,
|
|
320
286
|
"active": "N",
|
|
321
287
|
"used": "Y",
|
|
322
288
|
"group": 0
|
|
@@ -328,7 +294,7 @@
|
|
|
328
294
|
"masterPort": 5001,
|
|
329
295
|
"name": ">Grid",
|
|
330
296
|
"displayName": ">Grid",
|
|
331
|
-
"extendedType":
|
|
297
|
+
"extendedType": 0,
|
|
332
298
|
"active": "N",
|
|
333
299
|
"used": "Y",
|
|
334
300
|
"group": 0
|
|
@@ -340,11 +306,23 @@
|
|
|
340
306
|
"masterPort": 5001,
|
|
341
307
|
"name": ">Swimmingpool",
|
|
342
308
|
"displayName": ">Swimmingpool",
|
|
343
|
-
"extendedType":
|
|
309
|
+
"extendedType": 0,
|
|
344
310
|
"active": "N",
|
|
345
311
|
"used": "Y",
|
|
346
312
|
"group": 0
|
|
347
313
|
},
|
|
314
|
+
{
|
|
315
|
+
"logicalNodeAddress": 252,
|
|
316
|
+
"logicalAddress": 104,
|
|
317
|
+
"masterAddress": "192.168.0.97",
|
|
318
|
+
"masterPort": 5001,
|
|
319
|
+
"name": "Tim Small",
|
|
320
|
+
"displayName": "Tim Small",
|
|
321
|
+
"extendedType": 8,
|
|
322
|
+
"active": "Y",
|
|
323
|
+
"used": "Y",
|
|
324
|
+
"group": 0
|
|
325
|
+
},
|
|
348
326
|
{
|
|
349
327
|
"logicalNodeAddress": 252,
|
|
350
328
|
"logicalAddress": 7,
|
|
@@ -365,7 +343,7 @@
|
|
|
365
343
|
"name": "Garage $",
|
|
366
344
|
"displayName": "Garage ",
|
|
367
345
|
"extendedType": 8,
|
|
368
|
-
"active": "
|
|
346
|
+
"active": "Y",
|
|
369
347
|
"used": "Y",
|
|
370
348
|
"group": 0
|
|
371
349
|
},
|
|
@@ -376,7 +354,7 @@
|
|
|
376
354
|
"masterPort": 5001,
|
|
377
355
|
"name": "2|Dimmer",
|
|
378
356
|
"displayName": "Somfy Dimmer",
|
|
379
|
-
"extendedType":
|
|
357
|
+
"extendedType": 1,
|
|
380
358
|
"active": "Y",
|
|
381
359
|
"used": "Y",
|
|
382
360
|
"group": 0
|
|
@@ -388,7 +366,7 @@
|
|
|
388
366
|
"masterPort": 5001,
|
|
389
367
|
"name": "Blue LED",
|
|
390
368
|
"displayName": "Blue LED",
|
|
391
|
-
"extendedType":
|
|
369
|
+
"extendedType": 1,
|
|
392
370
|
"active": "N",
|
|
393
371
|
"used": "Y",
|
|
394
372
|
"group": 0
|
|
@@ -400,7 +378,7 @@
|
|
|
400
378
|
"masterPort": 5001,
|
|
401
379
|
"name": "Red LED",
|
|
402
380
|
"displayName": "Red LED",
|
|
403
|
-
"extendedType":
|
|
381
|
+
"extendedType": 1,
|
|
404
382
|
"active": "N",
|
|
405
383
|
"used": "Y",
|
|
406
384
|
"group": 0
|
|
@@ -483,11 +461,11 @@
|
|
|
483
461
|
"p1": false,
|
|
484
462
|
"proxy": {
|
|
485
463
|
"cloudServer": "ws.duotecno.eu",
|
|
486
|
-
"cloudPort":
|
|
464
|
+
"cloudPort": 5097,
|
|
487
465
|
"masterAddress": "192.168.0.97",
|
|
488
466
|
"masterPort": 5001,
|
|
489
467
|
"uniqueId": "",
|
|
490
468
|
"debug": false,
|
|
491
|
-
"kind": "
|
|
469
|
+
"kind": "gw"
|
|
492
470
|
}
|
|
493
471
|
}
|
package/duotecno/logger.js
CHANGED
package/duotecno/logger.ts
CHANGED
|
@@ -10,7 +10,7 @@ export enum LogLevel { noLog = 0, log = 1, debug = 2 };
|
|
|
10
10
|
export const logSettings = {
|
|
11
11
|
"*": LogLevel.log,
|
|
12
12
|
"master": LogLevel.log,
|
|
13
|
-
"protocol": LogLevel.
|
|
13
|
+
"protocol": LogLevel.noLog,
|
|
14
14
|
"webapp": LogLevel.log,
|
|
15
15
|
"p1": LogLevel.log,
|
|
16
16
|
"shelly": LogLevel.log,
|
package/duotecno/protocol.js
CHANGED
|
@@ -790,6 +790,7 @@ exports.Protocol = {
|
|
|
790
790
|
}
|
|
791
791
|
else if (next.cmd === Rec.Motor) {
|
|
792
792
|
// motor -> boolean/status
|
|
793
|
+
// [38,0,252,104,8,0,0]
|
|
793
794
|
// 0 = stopped, 1 stopped/down, 2 = stopped/up, 3 = busy/down, 4 = busy/up
|
|
794
795
|
unit.status = next.message[6];
|
|
795
796
|
unit.value = next.message[6];
|
|
@@ -800,7 +801,8 @@ exports.Protocol = {
|
|
|
800
801
|
// = EV_UNITMACROCOMMANDO
|
|
801
802
|
// examples: On 50%: [69,0,NodeAddress,UnitAddress,6,1,0,50]
|
|
802
803
|
// Off: [69,0,NodeAddress,UnitAddress,6,0,0,0] -> don't touch dimmer value
|
|
803
|
-
//
|
|
804
|
+
// unit_type_duoswitch -> Event = 32, state 0, 1, 2
|
|
805
|
+
// STATUS (Event: 7 = dimmer, 5 = switch, on = 1, off = 0)
|
|
804
806
|
if (((next.message[4] == 7) || (next.message[4] == 5)) && (next.message[5] == 1)) {
|
|
805
807
|
// PIR ON -- override status value with "2" (our PIR ON)
|
|
806
808
|
unit.status = 2;
|
|
@@ -815,6 +817,13 @@ exports.Protocol = {
|
|
|
815
817
|
(next.message[4] === 11)) {
|
|
816
818
|
unit.value = this.makeWord(next.message, 6);
|
|
817
819
|
}
|
|
820
|
+
else if (next.message[4] === 32) {
|
|
821
|
+
// unit_type_duoswitch -> Event = 32
|
|
822
|
+
// [69,0,252,104,32,2,0,0]
|
|
823
|
+
// state = 0 -> stop, state = 1 -> up, state = 2 -> down
|
|
824
|
+
unit.value = 10 + next.message[5];
|
|
825
|
+
unit.status = next.message[5];
|
|
826
|
+
}
|
|
818
827
|
(0, logger_1.debug)("protocol", "received status - macro -> value=" + unit.value + " / status=" + unit.status);
|
|
819
828
|
kind = "M";
|
|
820
829
|
}
|
package/duotecno/protocol.ts
CHANGED
|
@@ -913,6 +913,7 @@ export const Protocol = {
|
|
|
913
913
|
|
|
914
914
|
} else if (next.cmd === Rec.Motor) {
|
|
915
915
|
// motor -> boolean/status
|
|
916
|
+
// [38,0,252,104,8,0,0]
|
|
916
917
|
// 0 = stopped, 1 stopped/down, 2 = stopped/up, 3 = busy/down, 4 = busy/up
|
|
917
918
|
unit.status = next.message[6];
|
|
918
919
|
unit.value = next.message[6];
|
|
@@ -923,8 +924,9 @@ export const Protocol = {
|
|
|
923
924
|
// = EV_UNITMACROCOMMANDO
|
|
924
925
|
// examples: On 50%: [69,0,NodeAddress,UnitAddress,6,1,0,50]
|
|
925
926
|
// Off: [69,0,NodeAddress,UnitAddress,6,0,0,0] -> don't touch dimmer value
|
|
927
|
+
// unit_type_duoswitch -> Event = 32, state 0, 1, 2
|
|
926
928
|
|
|
927
|
-
// STATUS
|
|
929
|
+
// STATUS (Event: 7 = dimmer, 5 = switch, on = 1, off = 0)
|
|
928
930
|
if (((next.message[4] == 7) || (next.message[4] == 5)) && (next.message[5] == 1)) {
|
|
929
931
|
// PIR ON -- override status value with "2" (our PIR ON)
|
|
930
932
|
unit.status = 2;
|
|
@@ -939,6 +941,13 @@ export const Protocol = {
|
|
|
939
941
|
if ((((next.message[4] === 1) || (next.message[4] === 6) || (next.message[4] === 7)) && (next.message[5] === 1)) ||
|
|
940
942
|
(next.message[4] === 11)) {
|
|
941
943
|
unit.value = this.makeWord(next.message, 6);
|
|
944
|
+
|
|
945
|
+
} else if (next.message[4] === 32) {
|
|
946
|
+
// unit_type_duoswitch -> Event = 32
|
|
947
|
+
// [69,0,252,104,32,2,0,0]
|
|
948
|
+
// state = 0 -> stop, state = 1 -> up, state = 2 -> down
|
|
949
|
+
unit.value = 10 + next.message[5];
|
|
950
|
+
unit.status = next.message[5];
|
|
942
951
|
}
|
|
943
952
|
|
|
944
953
|
debug("protocol", "received status - macro -> value=" + unit.value + " / status=" + unit.status);
|
package/duotecno/types.js
CHANGED
|
@@ -154,7 +154,7 @@ var LinkType;
|
|
|
154
154
|
LinkType["kNoType"] = "";
|
|
155
155
|
LinkType["kAccessory"] = "accessory";
|
|
156
156
|
})(LinkType = exports.LinkType || (exports.LinkType = {}));
|
|
157
|
-
exports.kEmptyLink = Object.assign(Object.assign({}, exports.kEmptyUnit), { type: LinkType.
|
|
157
|
+
exports.kEmptyLink = Object.assign(Object.assign({}, exports.kEmptyUnit), { type: LinkType.kNoType, max: 100, min: 0, accName: "acc", accId: "", accService: [] });
|
|
158
158
|
exports.kEmptyBinding = {
|
|
159
159
|
channel: "0", masterAddress: "0.0.0.0", masterPort: 5001, register: 0
|
|
160
160
|
};
|
|
@@ -301,7 +301,9 @@ exports.Sanitizers = {
|
|
|
301
301
|
link.accId = aLink.accId || "";
|
|
302
302
|
link.accName = aLink.accName || "";
|
|
303
303
|
link.accService = aLink.accService || [];
|
|
304
|
-
link.type = aLink.type ||
|
|
304
|
+
link.type = aLink.type || exports.kEmptyLink.type;
|
|
305
|
+
link.max = aLink.max || exports.kEmptyLink.max;
|
|
306
|
+
link.min = aLink.min || exports.kEmptyLink.min;
|
|
305
307
|
return link;
|
|
306
308
|
},
|
|
307
309
|
switchConfig: function (aSwitch) {
|
package/duotecno/types.ts
CHANGED
|
@@ -269,13 +269,15 @@ export interface Link extends UnitDef{
|
|
|
269
269
|
accId: string;
|
|
270
270
|
accService: Array<string>; // e.g. ["On", "Brightness"], ["TargetPosition", "CurrentPosition", ...]
|
|
271
271
|
type: LinkType;
|
|
272
|
+
max?: number;
|
|
273
|
+
min?: number;
|
|
272
274
|
unit?: Unit;
|
|
273
275
|
dtValue?: number | boolean | string;
|
|
274
276
|
hbValue?: number | boolean | string
|
|
275
277
|
}
|
|
276
278
|
|
|
277
279
|
export const kEmptyLink: Link = {
|
|
278
|
-
...kEmptyUnit, type: LinkType.
|
|
280
|
+
...kEmptyUnit, type: LinkType.kNoType, max: 100, min: 0, accName: "acc", accId: "", accService: []
|
|
279
281
|
};
|
|
280
282
|
|
|
281
283
|
|
|
@@ -519,7 +521,9 @@ export const Sanitizers = {
|
|
|
519
521
|
link.accId = aLink.accId || "";
|
|
520
522
|
link.accName = aLink.accName || "";
|
|
521
523
|
link.accService = aLink.accService || [];
|
|
522
|
-
link.type = aLink.type ||
|
|
524
|
+
link.type = aLink.type || kEmptyLink.type;
|
|
525
|
+
link.max = aLink.max || kEmptyLink.max;
|
|
526
|
+
link.min = aLink.min || kEmptyLink.min;
|
|
523
527
|
return link;
|
|
524
528
|
},
|
|
525
529
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-smartsystem",
|
|
3
3
|
"displayname": "Duotecno Bridge",
|
|
4
|
-
"version": "6.8.
|
|
4
|
+
"version": "6.8.4",
|
|
5
5
|
"description": "SmartServer (Proxy Websockets to TCP sockets, Smappee MQTT, Duotecno IP Nodes, Homekit interface)",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"author": "Johan Coppieters",
|
package/server/HB.js
CHANGED
|
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.hbUnsubscribe = exports.hbSubscribe = exports.hbGetValue = exports.hbSetState = exports.dt2hbvalue = exports.hb2dtvalue = exports.hbLogin = exports.hbAccessories = exports.hbService = exports.hbName = void 0;
|
|
12
|
+
exports.hbUnsubscribe = exports.hbSubscribe = exports.hbStop = exports.hbGetValue = exports.hbSetState = exports.dt2hbvalue = exports.hb2dtvalue = exports.hbLogin = exports.hbAccessories = exports.hbService = exports.hbName = void 0;
|
|
13
13
|
const logger_1 = require("../duotecno/logger");
|
|
14
14
|
const HA_API_1 = require("./HA-API");
|
|
15
15
|
let gAccessories = [];
|
|
@@ -173,14 +173,15 @@ function hbLoad(value, service) {
|
|
|
173
173
|
return (value) ? 100 : 0;
|
|
174
174
|
}
|
|
175
175
|
else if (typeof value === "number") {
|
|
176
|
-
|
|
176
|
+
value = Math.round(value);
|
|
177
|
+
if (value <= 1) {
|
|
177
178
|
return 0;
|
|
178
179
|
}
|
|
179
|
-
else if (value >=
|
|
180
|
+
else if (value >= 99) {
|
|
180
181
|
return 100;
|
|
181
182
|
}
|
|
182
183
|
else {
|
|
183
|
-
return
|
|
184
|
+
return value;
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
187
|
}
|
|
@@ -217,16 +218,31 @@ function hb2dtvalue(unit, value) {
|
|
|
217
218
|
return { dtTarget, dtValue };
|
|
218
219
|
}
|
|
219
220
|
exports.hb2dtvalue = hb2dtvalue;
|
|
220
|
-
function dt2hbvalue(unit, dontChangeValue) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (unit.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
221
|
+
function dt2hbvalue(unit, dontChangeValue, min, max) {
|
|
222
|
+
// tunr into (rounded) number
|
|
223
|
+
let value = (typeof unit.value === "boolean") ? (unit.value ? max : min) : Math.round(unit.value);
|
|
224
|
+
// Virtual motor: state = 0 -> stop, state = 1 -> up, state = 2 -> down
|
|
225
|
+
// Real motor: 0 = stopped, 1 stopped/down, 2 = stopped/up, 3 = busy/down, 4 = busy/up
|
|
226
|
+
if (unit.isUpDown()) {
|
|
227
|
+
// don't send status of real motor
|
|
228
|
+
return (value == 10) ? -1 : // stop [ virtual ]
|
|
229
|
+
(value == 11) ? max : // up [ virtual ]
|
|
230
|
+
(value == 12) ? min : undefined; // down [ virtual ]
|
|
231
|
+
// return ((value === 1) || (value === 3)) ? min : // down [ motor ]
|
|
232
|
+
// ((value === 2) || (value === 4)) ? max : // up [ motor ]
|
|
233
|
+
// (value == 0) ? dontChangeValue : // no change [ motor ]
|
|
234
|
+
// (value == 10) ? -1 : // stop [ virtual ]
|
|
235
|
+
// (value == 11) ? max : min; // up/down [ virtual ]
|
|
236
|
+
}
|
|
237
|
+
else if (unit.isDimmer()) {
|
|
238
|
+
return (value <= 1) ? 0 : (value >= 99) ? 100 : value;
|
|
239
|
+
}
|
|
240
|
+
else if (unit.isSwitch() || unit.isMood()) {
|
|
241
|
+
return (value) ? max : min;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
230
246
|
}
|
|
231
247
|
exports.dt2hbvalue = dt2hbvalue;
|
|
232
248
|
function hbValue(data, service) {
|
|
@@ -276,6 +292,28 @@ function hbGetValue(server, id, service, token) {
|
|
|
276
292
|
});
|
|
277
293
|
}
|
|
278
294
|
exports.hbGetValue = hbGetValue;
|
|
295
|
+
function hbStop(server, id, service, token) {
|
|
296
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
297
|
+
// does not exist, but we'll do a getCurrentPosition + setTargetPosition to this value
|
|
298
|
+
const respBody = yield (0, HA_API_1.httpRequest)(server + "/api/accessories/" + id, 'GET', "", HA_API_1.ContentType.json, {
|
|
299
|
+
Authorization: 'Bearer ' + token
|
|
300
|
+
});
|
|
301
|
+
if (respBody && respBody.error) {
|
|
302
|
+
(0, logger_1.log)("HB", "hbGetState: " + id + " -> error: " + JSON.stringify(respBody));
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
else if (respBody && respBody.values) {
|
|
306
|
+
(0, logger_1.log)("HB", "hbStop GetState: " + id + " -> " + JSON.stringify(respBody.values));
|
|
307
|
+
const data = { value: respBody.values.CurrentPosition, characteristicType: "TargetPosition" };
|
|
308
|
+
(0, logger_1.log)("HB", "hbStop SetState: " + id + " -> " + JSON.stringify(data));
|
|
309
|
+
yield (0, HA_API_1.httpRequest)(server + "/api/accessories/" + id, 'PUT', JSON.stringify(data), HA_API_1.ContentType.json, {
|
|
310
|
+
Authorization: 'Bearer ' + token
|
|
311
|
+
});
|
|
312
|
+
return hbValue(respBody.values, service);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
exports.hbStop = hbStop;
|
|
279
317
|
function hbSubscribe(callback) {
|
|
280
318
|
if (timer) {
|
|
281
319
|
clearInterval(timer);
|
package/server/HB.ts
CHANGED
|
@@ -177,12 +177,13 @@ function hbLoad(value: number | boolean, service: Array<string>): Record<string,
|
|
|
177
177
|
if (typeof value === "boolean") {
|
|
178
178
|
return (value) ? 100 : 0;
|
|
179
179
|
} else if (typeof value === "number") {
|
|
180
|
-
|
|
180
|
+
value = Math.round(value);
|
|
181
|
+
if (value <= 1) {
|
|
181
182
|
return 0;
|
|
182
|
-
} else if (value >=
|
|
183
|
+
} else if (value >= 99) {
|
|
183
184
|
return 100;
|
|
184
185
|
} else {
|
|
185
|
-
return
|
|
186
|
+
return value;
|
|
186
187
|
}
|
|
187
188
|
}
|
|
188
189
|
}
|
|
@@ -223,16 +224,32 @@ export function hb2dtvalue(unit: Unit, value: number | boolean): {dtValue: numbe
|
|
|
223
224
|
return {dtTarget, dtValue};
|
|
224
225
|
}
|
|
225
226
|
|
|
226
|
-
export function dt2hbvalue(unit: Unit, dontChangeValue: number): number {
|
|
227
|
-
|
|
227
|
+
export function dt2hbvalue(unit: Unit, dontChangeValue: number, min: number, max: number): number {
|
|
228
|
+
// tunr into (rounded) number
|
|
229
|
+
let value = (typeof unit.value === "boolean") ? (unit.value ? max : min) : Math.round(unit.value);
|
|
228
230
|
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
231
|
+
// Virtual motor: state = 0 -> stop, state = 1 -> up, state = 2 -> down
|
|
232
|
+
// Real motor: 0 = stopped, 1 stopped/down, 2 = stopped/up, 3 = busy/down, 4 = busy/up
|
|
233
|
+
if (unit.isUpDown()) {
|
|
234
|
+
// don't send status of real motor
|
|
235
|
+
return (value == 10) ? -1 : // stop [ virtual ]
|
|
236
|
+
(value == 11) ? max : // up [ virtual ]
|
|
237
|
+
(value == 12) ? min : undefined; // down [ virtual ]
|
|
238
|
+
// return ((value === 1) || (value === 3)) ? min : // down [ motor ]
|
|
239
|
+
// ((value === 2) || (value === 4)) ? max : // up [ motor ]
|
|
240
|
+
// (value == 0) ? dontChangeValue : // no change [ motor ]
|
|
241
|
+
// (value == 10) ? -1 : // stop [ virtual ]
|
|
242
|
+
// (value == 11) ? max : min; // up/down [ virtual ]
|
|
243
|
+
|
|
244
|
+
} else if (unit.isDimmer()) {
|
|
245
|
+
return (value <= 1) ? 0 : (value >= 99) ? 100 : value;
|
|
246
|
+
|
|
247
|
+
} else if (unit.isSwitch() || unit.isMood()) {
|
|
248
|
+
return (value) ? max : min;
|
|
234
249
|
|
|
235
|
-
|
|
250
|
+
} else {
|
|
251
|
+
return value;
|
|
252
|
+
}
|
|
236
253
|
}
|
|
237
254
|
|
|
238
255
|
function hbValue(data: Record<string, any>, service: Array<string>): number {
|
|
@@ -282,6 +299,28 @@ export async function hbGetValue(server: string, id: string, service: Array<stri
|
|
|
282
299
|
};
|
|
283
300
|
}
|
|
284
301
|
|
|
302
|
+
export async function hbStop(server: string, id: string, service: Array<string>, token: string) : Promise<number | undefined> {
|
|
303
|
+
// does not exist, but we'll do a getCurrentPosition + setTargetPosition to this value
|
|
304
|
+
const respBody = await httpRequest(server + "/api/accessories/"+id, 'GET', "", ContentType.json, {
|
|
305
|
+
Authorization: 'Bearer ' + token
|
|
306
|
+
});
|
|
307
|
+
if (respBody && respBody.error) {
|
|
308
|
+
log("HB", "hbGetState: " + id + " -> error: " + JSON.stringify(respBody));
|
|
309
|
+
return undefined;
|
|
310
|
+
|
|
311
|
+
} else if (respBody && respBody.values) {
|
|
312
|
+
log("HB", "hbStop GetState: " + id + " -> " + JSON.stringify(respBody.values));
|
|
313
|
+
|
|
314
|
+
const data = {value: respBody.values.CurrentPosition, characteristicType: "TargetPosition"};
|
|
315
|
+
log("HB", "hbStop SetState: " + id + " -> " + JSON.stringify(data));
|
|
316
|
+
|
|
317
|
+
await httpRequest(server + "/api/accessories/"+id, 'PUT', JSON.stringify(data), ContentType.json, {
|
|
318
|
+
Authorization: 'Bearer ' + token
|
|
319
|
+
});
|
|
320
|
+
return hbValue(respBody.values, service);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
285
324
|
export function hbSubscribe(callback) {
|
|
286
325
|
if (timer) {
|
|
287
326
|
clearInterval(timer);
|
package/server/proxy.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Context = exports.cleanStart = exports.makeNewCloudConnection = exports.setProxyConfig = exports.kEmptyProxy = void 0;
|
|
4
4
|
const net = require("net");
|
|
5
|
-
const WebSocket = require("ws");
|
|
6
5
|
const child_process_1 = require("child_process");
|
|
7
6
|
const process_1 = require("process");
|
|
8
7
|
exports.kEmptyProxy = {
|
|
@@ -61,17 +60,23 @@ function error(str) {
|
|
|
61
60
|
///////////////////////////////
|
|
62
61
|
// Start up the proxy server //
|
|
63
62
|
///////////////////////////////
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
if (config.uniqueId) {
|
|
64
|
+
makeNewCloudConnection(config.masterAddress, config.masterPort, config.cloudServer, config.cloudPort, config.uniqueId);
|
|
65
|
+
}
|
|
67
66
|
function makeNewCloudConnection(master, masterPort, server, serverPort, uniqueId) {
|
|
68
|
-
const cloudSocket = new
|
|
69
|
-
cloudSocket.
|
|
67
|
+
const cloudSocket = new net.Socket();
|
|
68
|
+
cloudSocket.connect(serverPort, server, () => {
|
|
70
69
|
log(`[PROXY] → [CLOUD] Connected to Cloud at ${server}:${serverPort}`);
|
|
70
|
+
// Send unique ID as the first message to identify this proxy/device
|
|
71
|
+
cloudSocket.write(`[${uniqueId}]`);
|
|
72
|
+
debug(`[PROXY] → [CLOUD] Sent unique ID: ${uniqueId}`);
|
|
71
73
|
const context = new Context(cloudSocket, master, masterPort, server, serverPort, uniqueId);
|
|
72
74
|
gCloudConnections.push(context);
|
|
73
75
|
log(`[PROXY] → [CLOUD] New free connection #${gCloudConnections.length} to the Cloud at ${server}:${serverPort}`);
|
|
74
76
|
});
|
|
77
|
+
cloudSocket.on('error', (err) => {
|
|
78
|
+
error(`[PROXY] → [CLOUD] Failed to connect to Cloud: ${err.message}`);
|
|
79
|
+
});
|
|
75
80
|
log(`[PROXY] → [CLOUD] Attempting to start a new free connection for ${config.uniqueId} to the Cloud at ${server}:${serverPort}`);
|
|
76
81
|
}
|
|
77
82
|
exports.makeNewCloudConnection = makeNewCloudConnection;
|
|
@@ -85,7 +90,7 @@ function cleanStart(restart = false) {
|
|
|
85
90
|
context.deviceSocket.end();
|
|
86
91
|
context.deviceSocket = null;
|
|
87
92
|
}
|
|
88
|
-
context.cloudSocket.
|
|
93
|
+
context.cloudSocket.end();
|
|
89
94
|
context.cloudSocket = null;
|
|
90
95
|
});
|
|
91
96
|
gCloudConnections.splice(0, gCloudConnections.length); // clear the array
|
|
@@ -117,11 +122,11 @@ class Context {
|
|
|
117
122
|
}
|
|
118
123
|
setupCloudSocket() {
|
|
119
124
|
// set up handlers for the cloud socket
|
|
120
|
-
this.cloudSocket.on('
|
|
121
|
-
this.handleDataFromCloud(
|
|
125
|
+
this.cloudSocket.on('data', data => {
|
|
126
|
+
this.handleDataFromCloud(data);
|
|
122
127
|
});
|
|
123
|
-
this.cloudSocket.on('close', (
|
|
124
|
-
warning('[CLOUD] Connection closed
|
|
128
|
+
this.cloudSocket.on('close', () => {
|
|
129
|
+
warning('[CLOUD] Connection closed');
|
|
125
130
|
if (this.deviceSocket) {
|
|
126
131
|
this.deviceSocket.end();
|
|
127
132
|
this.deviceSocket = null;
|
|
@@ -136,26 +141,31 @@ class Context {
|
|
|
136
141
|
});
|
|
137
142
|
}
|
|
138
143
|
handleDataFromCloud(data) {
|
|
139
|
-
debug(`[CLOUD] → [PROXY]: Data from Cloud: ${data.toString()
|
|
144
|
+
debug(`[CLOUD] → [PROXY]: Data from Cloud: ${data.toString()}`);
|
|
140
145
|
if (!this.deviceSocket) {
|
|
141
146
|
// we either have a real client that wants to connect to the device,
|
|
142
147
|
// or we receive a heartbeat from the cloud server
|
|
148
|
+
// or we receive a connection response [OK], [OK-=2], [ERROR-...]
|
|
143
149
|
if (this.isHeartbeatRequest(data)) {
|
|
144
150
|
debug(`[CLOUD] → [PROXY]: Received heartbeat from Cloud, returning response.`);
|
|
145
151
|
// respond to the heartbeat of the cloud server
|
|
146
|
-
this.cloudSocket.
|
|
152
|
+
this.cloudSocket.write("[72,3]"); // command([Command.HeartbeatStatus, CommandMethod.Heartbeat.server])
|
|
153
|
+
}
|
|
154
|
+
else if (this.isConnectionResponse(data)) {
|
|
155
|
+
debug(`[CLOUD] → [PROXY]: Received connection response from Cloud: ${data.toString()}`);
|
|
156
|
+
// Handle connection response from the cloud server
|
|
147
157
|
}
|
|
148
158
|
else {
|
|
149
|
-
// There is incomming data, it's a fresh connection, so: a new client wants to connect to the device
|
|
159
|
+
// There is real incomming data, it's a fresh connection, so: a new client wants to connect to the device
|
|
150
160
|
// 1) create a new (free) connection to the cloud server for the next client
|
|
151
161
|
// 2) connect to the device and send the initial data
|
|
152
|
-
log(`[PROXY] → [CLOUD] New client connection, creating new free connection for this proxy/device: ${
|
|
162
|
+
log(`[PROXY] → [CLOUD] New client connection, creating new free connection for this proxy/device: ${config.uniqueId}.`);
|
|
153
163
|
makeNewCloudConnection(this.master, this.masterPort, this.server, this.serverPort, this.uniqueId);
|
|
154
164
|
this.makeDeviceConnection(this.master, this.masterPort, data);
|
|
155
165
|
}
|
|
156
166
|
}
|
|
157
167
|
else if (this.deviceSocket.readyState === 'open') {
|
|
158
|
-
this.deviceSocket.write(data);
|
|
168
|
+
this.deviceSocket.write(new Uint8Array(data));
|
|
159
169
|
debug(`[PROXY] → [DEVICE] forwarding data to device: ${data.toString().slice(0, -1)}`);
|
|
160
170
|
}
|
|
161
171
|
else {
|
|
@@ -168,25 +178,34 @@ class Context {
|
|
|
168
178
|
this.deviceSocket = new net.Socket();
|
|
169
179
|
// Connect to local device and send the data that came in from the cloud
|
|
170
180
|
this.deviceSocket.connect(port, address, () => {
|
|
181
|
+
// Check if socket is still valid (not cleaned up by removeConnection)
|
|
182
|
+
if (!this.deviceSocket) {
|
|
183
|
+
warning(`[PROXY] → [DEVICE] Device socket was cleaned up before connection completed`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
171
186
|
log(`[PROXY] → [DEVICE] Connected to local device at ${address}:${port}`);
|
|
172
187
|
this.setUpDeviceSocket();
|
|
173
188
|
log(`[PROXY] → [DEVICE] Sending initial message: ${data.toString().slice(0, -1)}`);
|
|
174
|
-
this.deviceSocket.write(data);
|
|
189
|
+
this.deviceSocket.write(new Uint8Array(data));
|
|
175
190
|
});
|
|
176
191
|
}
|
|
177
192
|
else if (this.deviceSocket.readyState === 'open') {
|
|
178
193
|
warning(`[PROXY] → [DEVICE] Device socket already open, sending data directly -- STRANGE !!`);
|
|
179
|
-
this.deviceSocket.write(data);
|
|
194
|
+
this.deviceSocket.write(new Uint8Array(data));
|
|
180
195
|
}
|
|
181
196
|
else {
|
|
182
197
|
error(`[PROXY] → [DEVICE] Device socket exists, but is not open yet !!`);
|
|
183
198
|
}
|
|
184
199
|
}
|
|
185
200
|
setUpDeviceSocket() {
|
|
201
|
+
if (!this.deviceSocket) {
|
|
202
|
+
error(`[PROXY] → [DEVICE] Cannot set up device socket - socket is null`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
186
205
|
this.deviceSocket.on('data', (data) => {
|
|
187
206
|
const message = data.toString('utf-8');
|
|
188
207
|
debug(`[DEVICE] → [PROXY] forwarding data to Cloud: ${message.slice(0, -1)}`);
|
|
189
|
-
this.cloudSocket.
|
|
208
|
+
this.cloudSocket.write(message);
|
|
190
209
|
});
|
|
191
210
|
this.deviceSocket.on('close', () => {
|
|
192
211
|
log(`[DEVICE] → [PROXY] Device socket closed`);
|
|
@@ -198,11 +217,17 @@ class Context {
|
|
|
198
217
|
});
|
|
199
218
|
}
|
|
200
219
|
removeConnection() {
|
|
201
|
-
|
|
220
|
+
// Clean up device socket
|
|
221
|
+
if (this.deviceSocket) {
|
|
222
|
+
if (!this.deviceSocket.destroyed) {
|
|
223
|
+
this.deviceSocket.end();
|
|
224
|
+
}
|
|
225
|
+
this.deviceSocket = null;
|
|
226
|
+
}
|
|
202
227
|
// remove this context and close the cloud socket
|
|
203
228
|
const index = gCloudConnections.indexOf(this);
|
|
204
229
|
if (index !== -1) {
|
|
205
|
-
this.cloudSocket.
|
|
230
|
+
this.cloudSocket.end();
|
|
206
231
|
this.cloudSocket = null;
|
|
207
232
|
gCloudConnections.splice(index, 1);
|
|
208
233
|
log(`[PROXY] → [CLOUD] Closed socket to Cloud and removed context for device ${this.master}`);
|
|
@@ -238,19 +263,20 @@ class Context {
|
|
|
238
263
|
}
|
|
239
264
|
// check if it's a heartbeat request from the server
|
|
240
265
|
isHeartbeatRequest(data) {
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
266
|
+
// hearbeatKind = (poll = 1, poll_old = 2, serve = 3)
|
|
267
|
+
return ((data[0] === 91) && // [
|
|
268
|
+
(data[1] === 50) && // 2
|
|
269
|
+
(data[2] === 49) && // 1
|
|
270
|
+
(data[3] === 53) && // 5
|
|
271
|
+
(data[4] === 44) && // ,
|
|
272
|
+
(data[5] === 51) && // 3
|
|
273
|
+
(data[6] === 93)); // ]
|
|
274
|
+
}
|
|
275
|
+
// check if it's a server response after connecting and sending our uniqueID
|
|
276
|
+
isConnectionResponse(data) {
|
|
277
|
+
const message = data.toString();
|
|
278
|
+
// Check for various server response patterns
|
|
279
|
+
return (message.startsWith('[OK')) || (message.startsWith('[ERROR'));
|
|
254
280
|
}
|
|
255
281
|
}
|
|
256
282
|
exports.Context = Context;
|
package/server/proxy.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import * as net from 'net';
|
|
2
|
-
import * as WebSocket from 'ws';
|
|
3
2
|
import { spawn } from 'child_process';
|
|
4
3
|
import { argv, cwd, execPath } from 'process';
|
|
5
4
|
|
|
6
5
|
//
|
|
7
|
-
// [ device (TCP) ] ←→ [ Proxy =
|
|
6
|
+
// [ device (TCP) ] ←→ [ Proxy = TCP client ] ←→ [ Cloud = TCP server ] ←→ [ browser client ]
|
|
8
7
|
//
|
|
9
8
|
|
|
10
9
|
export interface ProxyConfig {
|
|
@@ -16,6 +15,7 @@ export interface ProxyConfig {
|
|
|
16
15
|
debug?: boolean; // debug mode
|
|
17
16
|
kind?: "pm2" | "gw" | "solo";
|
|
18
17
|
}
|
|
18
|
+
|
|
19
19
|
export const kEmptyProxy: ProxyConfig = {
|
|
20
20
|
cloudServer: "ws.duotecno.eu",
|
|
21
21
|
cloudPort: 5098,
|
|
@@ -50,6 +50,8 @@ try {
|
|
|
50
50
|
console.log("No config-proxy.json found, using default values from homebridge or other.");
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
|
|
54
|
+
|
|
53
55
|
const gCloudConnections: Array<Context> = [];
|
|
54
56
|
const kDebug = config.debug || false; // enable debug mode from config
|
|
55
57
|
|
|
@@ -75,26 +77,34 @@ function error(str: string) {
|
|
|
75
77
|
///////////////////////////////
|
|
76
78
|
// Start up the proxy server //
|
|
77
79
|
///////////////////////////////
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// }
|
|
80
|
+
if (config.uniqueId) {
|
|
81
|
+
makeNewCloudConnection(config.masterAddress, config.masterPort, config.cloudServer, config.cloudPort, config.uniqueId);
|
|
82
|
+
}
|
|
82
83
|
|
|
83
84
|
|
|
84
85
|
export function makeNewCloudConnection(master: string, masterPort: number, server: string, serverPort: number, uniqueId: string) {
|
|
85
|
-
const cloudSocket = new
|
|
86
|
-
|
|
87
|
-
cloudSocket.
|
|
86
|
+
const cloudSocket = new net.Socket();
|
|
87
|
+
|
|
88
|
+
cloudSocket.connect(serverPort, server, () => {
|
|
88
89
|
log(`[PROXY] → [CLOUD] Connected to Cloud at ${server}:${serverPort}`);
|
|
90
|
+
|
|
91
|
+
// Send unique ID as the first message to identify this proxy/device
|
|
92
|
+
cloudSocket.write(`[${uniqueId}]`);
|
|
93
|
+
debug(`[PROXY] → [CLOUD] Sent unique ID: ${uniqueId}`);
|
|
89
94
|
|
|
90
95
|
const context = new Context(cloudSocket, master, masterPort, server, serverPort, uniqueId);
|
|
91
96
|
gCloudConnections.push(context);
|
|
92
97
|
log(`[PROXY] → [CLOUD] New free connection #${gCloudConnections.length} to the Cloud at ${server}:${serverPort}`);
|
|
93
98
|
});
|
|
94
99
|
|
|
100
|
+
cloudSocket.on('error', (err) => {
|
|
101
|
+
error(`[PROXY] → [CLOUD] Failed to connect to Cloud: ${err.message}`);
|
|
102
|
+
});
|
|
103
|
+
|
|
95
104
|
log(`[PROXY] → [CLOUD] Attempting to start a new free connection for ${config.uniqueId} to the Cloud at ${server}:${serverPort}`);
|
|
96
105
|
}
|
|
97
106
|
|
|
107
|
+
|
|
98
108
|
////////////////
|
|
99
109
|
// Restarting //
|
|
100
110
|
////////////////
|
|
@@ -105,7 +115,7 @@ export function cleanStart(restart: boolean = false) {
|
|
|
105
115
|
context.deviceSocket.end();
|
|
106
116
|
context.deviceSocket = null;
|
|
107
117
|
}
|
|
108
|
-
context.cloudSocket.
|
|
118
|
+
context.cloudSocket.end();
|
|
109
119
|
context.cloudSocket = null;
|
|
110
120
|
});
|
|
111
121
|
gCloudConnections.splice(0, gCloudConnections.length); // clear the array
|
|
@@ -129,14 +139,14 @@ export class Context {
|
|
|
129
139
|
master: string;
|
|
130
140
|
masterPort: number;
|
|
131
141
|
|
|
132
|
-
cloudSocket:
|
|
142
|
+
cloudSocket: net.Socket;
|
|
133
143
|
server: string;
|
|
134
144
|
serverPort: number;
|
|
135
145
|
|
|
136
146
|
// unique identifier for this device-gateway combination
|
|
137
147
|
uniqueId: string;
|
|
138
148
|
|
|
139
|
-
constructor(cloudSocket:
|
|
149
|
+
constructor(cloudSocket: net.Socket, master: string, masterPort: number, server: string, serverPort: number, uniqueId: string) {
|
|
140
150
|
this.deviceSocket = null;
|
|
141
151
|
this.master = master;
|
|
142
152
|
this.masterPort = masterPort;
|
|
@@ -152,12 +162,12 @@ export class Context {
|
|
|
152
162
|
|
|
153
163
|
setupCloudSocket() {
|
|
154
164
|
// set up handlers for the cloud socket
|
|
155
|
-
this.cloudSocket.on('
|
|
156
|
-
this.handleDataFromCloud(
|
|
165
|
+
this.cloudSocket.on('data', data => {
|
|
166
|
+
this.handleDataFromCloud(data as Buffer);
|
|
157
167
|
});
|
|
158
168
|
|
|
159
|
-
this.cloudSocket.on('close', (
|
|
160
|
-
warning('[CLOUD] Connection closed
|
|
169
|
+
this.cloudSocket.on('close', () => {
|
|
170
|
+
warning('[CLOUD] Connection closed');
|
|
161
171
|
if (this.deviceSocket) {
|
|
162
172
|
this.deviceSocket.end();
|
|
163
173
|
this.deviceSocket = null;
|
|
@@ -174,28 +184,33 @@ export class Context {
|
|
|
174
184
|
}
|
|
175
185
|
|
|
176
186
|
handleDataFromCloud(data: Buffer) {
|
|
177
|
-
debug(`[CLOUD] → [PROXY]: Data from Cloud: ${data.toString()
|
|
187
|
+
debug(`[CLOUD] → [PROXY]: Data from Cloud: ${data.toString()}`);
|
|
178
188
|
|
|
179
189
|
if (!this.deviceSocket) {
|
|
180
190
|
// we either have a real client that wants to connect to the device,
|
|
181
191
|
// or we receive a heartbeat from the cloud server
|
|
192
|
+
// or we receive a connection response [OK], [OK-=2], [ERROR-...]
|
|
182
193
|
|
|
183
194
|
if (this.isHeartbeatRequest(data)) {
|
|
184
195
|
debug(`[CLOUD] → [PROXY]: Received heartbeat from Cloud, returning response.`);
|
|
185
196
|
// respond to the heartbeat of the cloud server
|
|
186
|
-
this.cloudSocket.
|
|
197
|
+
this.cloudSocket.write("[72,3]"); // command([Command.HeartbeatStatus, CommandMethod.Heartbeat.server])
|
|
187
198
|
|
|
199
|
+
} else if (this.isConnectionResponse(data)) {
|
|
200
|
+
debug(`[CLOUD] → [PROXY]: Received connection response from Cloud: ${data.toString()}`);
|
|
201
|
+
// Handle connection response from the cloud server
|
|
202
|
+
|
|
188
203
|
} else {
|
|
189
|
-
// There is incomming data, it's a fresh connection, so: a new client wants to connect to the device
|
|
204
|
+
// There is real incomming data, it's a fresh connection, so: a new client wants to connect to the device
|
|
190
205
|
// 1) create a new (free) connection to the cloud server for the next client
|
|
191
206
|
// 2) connect to the device and send the initial data
|
|
192
|
-
log(`[PROXY] → [CLOUD] New client connection, creating new free connection for this proxy/device: ${
|
|
207
|
+
log(`[PROXY] → [CLOUD] New client connection, creating new free connection for this proxy/device: ${config.uniqueId}.`);
|
|
193
208
|
makeNewCloudConnection(this.master, this.masterPort, this.server, this.serverPort, this.uniqueId);
|
|
194
209
|
this.makeDeviceConnection(this.master, this.masterPort, data);
|
|
195
210
|
}
|
|
196
211
|
|
|
197
212
|
} else if (this.deviceSocket.readyState === 'open') {
|
|
198
|
-
this.deviceSocket.write(data);
|
|
213
|
+
this.deviceSocket.write(new Uint8Array(data));
|
|
199
214
|
debug(`[PROXY] → [DEVICE] forwarding data to device: ${data.toString().slice(0, -1)}`);
|
|
200
215
|
|
|
201
216
|
} else {
|
|
@@ -212,17 +227,23 @@ export class Context {
|
|
|
212
227
|
|
|
213
228
|
// Connect to local device and send the data that came in from the cloud
|
|
214
229
|
this.deviceSocket.connect(port, address, () => {
|
|
230
|
+
// Check if socket is still valid (not cleaned up by removeConnection)
|
|
231
|
+
if (!this.deviceSocket) {
|
|
232
|
+
warning(`[PROXY] → [DEVICE] Device socket was cleaned up before connection completed`);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
215
236
|
log(`[PROXY] → [DEVICE] Connected to local device at ${address}:${port}`);
|
|
216
237
|
|
|
217
238
|
this.setUpDeviceSocket();
|
|
218
239
|
|
|
219
240
|
log(`[PROXY] → [DEVICE] Sending initial message: ${data.toString().slice(0, -1)}`);
|
|
220
|
-
this.deviceSocket.write(data);
|
|
241
|
+
this.deviceSocket.write(new Uint8Array(data));
|
|
221
242
|
});
|
|
222
243
|
|
|
223
244
|
} else if (this.deviceSocket.readyState === 'open') {
|
|
224
245
|
warning(`[PROXY] → [DEVICE] Device socket already open, sending data directly -- STRANGE !!`);
|
|
225
|
-
this.deviceSocket.write(data);
|
|
246
|
+
this.deviceSocket.write(new Uint8Array(data));
|
|
226
247
|
|
|
227
248
|
} else {
|
|
228
249
|
error(`[PROXY] → [DEVICE] Device socket exists, but is not open yet !!`);
|
|
@@ -230,10 +251,15 @@ export class Context {
|
|
|
230
251
|
}
|
|
231
252
|
|
|
232
253
|
setUpDeviceSocket() {
|
|
254
|
+
if (!this.deviceSocket) {
|
|
255
|
+
error(`[PROXY] → [DEVICE] Cannot set up device socket - socket is null`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
233
259
|
this.deviceSocket.on('data', (data: Buffer) => {
|
|
234
260
|
const message = data.toString('utf-8');
|
|
235
261
|
debug(`[DEVICE] → [PROXY] forwarding data to Cloud: ${message.slice(0, -1)}`);
|
|
236
|
-
this.cloudSocket.
|
|
262
|
+
this.cloudSocket.write(message);
|
|
237
263
|
});
|
|
238
264
|
|
|
239
265
|
this.deviceSocket.on('close', () => {
|
|
@@ -248,12 +274,18 @@ export class Context {
|
|
|
248
274
|
}
|
|
249
275
|
|
|
250
276
|
removeConnection() {
|
|
251
|
-
|
|
277
|
+
// Clean up device socket
|
|
278
|
+
if (this.deviceSocket) {
|
|
279
|
+
if (!this.deviceSocket.destroyed) {
|
|
280
|
+
this.deviceSocket.end();
|
|
281
|
+
}
|
|
282
|
+
this.deviceSocket = null;
|
|
283
|
+
}
|
|
252
284
|
|
|
253
285
|
// remove this context and close the cloud socket
|
|
254
286
|
const index = gCloudConnections.indexOf(this);
|
|
255
287
|
if (index !== -1) {
|
|
256
|
-
this.cloudSocket.
|
|
288
|
+
this.cloudSocket.end();
|
|
257
289
|
this.cloudSocket = null;
|
|
258
290
|
gCloudConnections.splice(index, 1);
|
|
259
291
|
log(`[PROXY] → [CLOUD] Closed socket to Cloud and removed context for device ${this.master}`);
|
|
@@ -262,7 +294,6 @@ export class Context {
|
|
|
262
294
|
if (gCloudConnections.length === 0) {
|
|
263
295
|
log(`[PROXY] → [CLOUD] No more cloud connections, restarting proxy.`);
|
|
264
296
|
// just to be sure: restart...
|
|
265
|
-
|
|
266
297
|
if (config.kind === "pm2") {
|
|
267
298
|
// PM2 should restart us.
|
|
268
299
|
log(`[PROXY] → [CLOUD] PM2 should restart us, exiting now.`);
|
|
@@ -297,19 +328,21 @@ export class Context {
|
|
|
297
328
|
|
|
298
329
|
// check if it's a heartbeat request from the server
|
|
299
330
|
isHeartbeatRequest(data: Buffer): boolean {
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
331
|
+
// hearbeatKind = (poll = 1, poll_old = 2, serve = 3)
|
|
332
|
+
return ((data[0] === 91) && // [
|
|
333
|
+
(data[1] === 50) && // 2
|
|
334
|
+
(data[2] === 49) && // 1
|
|
335
|
+
(data[3] === 53) && // 5
|
|
336
|
+
(data[4] === 44) && // ,
|
|
337
|
+
(data[5] === 51) && // 3
|
|
338
|
+
(data[6] === 93)); // ]
|
|
339
|
+
}
|
|
304
340
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
(data[5] === 51) && // 3
|
|
312
|
-
(data[6] === 93)); // ]
|
|
313
|
-
}
|
|
341
|
+
// check if it's a server response after connecting and sending our uniqueID
|
|
342
|
+
isConnectionResponse(data: Buffer): boolean {
|
|
343
|
+
const message = data.toString();
|
|
344
|
+
|
|
345
|
+
// Check for various server response patterns
|
|
346
|
+
return (message.startsWith('[OK')) || (message.startsWith('[ERROR'));
|
|
314
347
|
}
|
|
315
348
|
}
|
package/server/smartapp.js
CHANGED
|
@@ -460,14 +460,22 @@ class SmartApp extends webapp_1.WebApp {
|
|
|
460
460
|
(0, logger_1.debug)("smartapp", "Received DT value from unit: " + u.getDisplayName() + " -> " + u.value + " / " + u.status + " from unit");
|
|
461
461
|
for (const link of this.links) {
|
|
462
462
|
if (u.isUnit(link.masterAddress, link.masterPort, link.logicalNodeAddress, link.logicalAddress)) {
|
|
463
|
-
const hbValue = (0, HB_1.dt2hbvalue)(u, link.hbValue);
|
|
464
|
-
if (
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
463
|
+
const hbValue = (0, HB_1.dt2hbvalue)(u, link.hbValue, link.min, link.max);
|
|
464
|
+
if (typeof hbValue != "undefined") {
|
|
465
|
+
if (hbValue === -1) {
|
|
466
|
+
(0, logger_1.log)("smartapp", "-> Virtual unit send 'stop' for accessory: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
|
|
467
|
+
const val = yield (0, HB_1.hbStop)(this.config.apiHost + ":" + this.config.apiPort, link.accId, link.accService, this.token);
|
|
468
|
+
if (typeof val != "undefined")
|
|
469
|
+
link.hbValue = val;
|
|
470
|
+
}
|
|
471
|
+
else if (hbValue !== link.hbValue) {
|
|
472
|
+
(0, logger_1.log)("smartapp", "-> Setting HB value for accessory: " + link.accName + " to " + hbValue + " of Duotecno unit: " + u.getDisplayName() + " with value " + u.value);
|
|
473
|
+
link.hbValue = hbValue;
|
|
474
|
+
yield (0, HB_1.hbSetState)(this.config.apiHost + ":" + this.config.apiPort, link.accId, link.accService, hbValue, this.token);
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
(0, logger_1.debug)("smartapp", "-> No change for accessory: " + link.accName + " to " + u.value + " / " + u.status);
|
|
478
|
+
}
|
|
471
479
|
}
|
|
472
480
|
}
|
|
473
481
|
}
|
|
@@ -504,7 +512,7 @@ class SmartApp extends webapp_1.WebApp {
|
|
|
504
512
|
let message;
|
|
505
513
|
try {
|
|
506
514
|
if (context.action === "add") {
|
|
507
|
-
this.links.push(types_1.kEmptyLink);
|
|
515
|
+
this.links.push(Object.assign(Object.assign({}, types_1.kEmptyLink), { type: types_1.LinkType.kAccessory }));
|
|
508
516
|
inx = this.links.length - 1;
|
|
509
517
|
return this.ejs("linkDetail", context, { link: this.links[inx], id: inx,
|
|
510
518
|
masters: this.system.masters, accessories, units });
|
|
@@ -536,11 +544,15 @@ class SmartApp extends webapp_1.WebApp {
|
|
|
536
544
|
// for now only accessory links.
|
|
537
545
|
const stype = context.getParam({ name: "type", type: "string", default: types_1.LinkType.kAccessory });
|
|
538
546
|
const name = context.getParam({ name: "name", type: "string", default: "--" });
|
|
547
|
+
// max & min values
|
|
548
|
+
const min = context.getParam({ name: "min", type: "integer", default: 0 });
|
|
549
|
+
const max = context.getParam({ name: "max", type: "integer", default: 100 });
|
|
539
550
|
// uniqiue id for the accessory
|
|
540
551
|
const accId = context.getParam({ name: "accId", type: "string", default: "" });
|
|
541
552
|
const accName = (0, HB_1.hbName)(accId);
|
|
542
553
|
const accService = (0, HB_1.hbService)(accId);
|
|
543
|
-
return { accId, accName, accService, name, masterAddress, masterPort, logicalAddress, logicalNodeAddress, type: stype };
|
|
554
|
+
return { accId, accName, accService, name, masterAddress, masterPort, logicalAddress, logicalNodeAddress, type: stype, min, max };
|
|
555
|
+
;
|
|
544
556
|
}
|
|
545
557
|
updateLink(inx, link) {
|
|
546
558
|
if ((inx >= 0) && (inx < this.links.length)) {
|
package/server/smartapp.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { Shelly } from './shelly';
|
|
|
22
22
|
import { P1 } from './p1';
|
|
23
23
|
import { setToken } from "../duotecno/config";
|
|
24
24
|
import { PowerBase } from "./powerbase";
|
|
25
|
-
import { dt2hbvalue, hb2dtvalue, hbAccessories, hbGetValue, hbLogin, hbName, hbService, hbSetState, hbSubscribe } from "./HB";
|
|
25
|
+
import { dt2hbvalue, hb2dtvalue, hbAccessories, hbGetValue, hbLogin, hbName, hbService, hbSetState, hbStop, hbSubscribe } from "./HB";
|
|
26
26
|
import { httpRequest, ContentType } from "./HA-API";
|
|
27
27
|
import { cleanStart, kEmptyProxy, ProxyConfig, setProxyConfig } from "./proxy";
|
|
28
28
|
|
|
@@ -279,7 +279,7 @@ export class SmartApp extends WebApp {
|
|
|
279
279
|
|
|
280
280
|
} else if (context.action === "restart") {
|
|
281
281
|
cleanStart(true);
|
|
282
|
-
|
|
282
|
+
|
|
283
283
|
} else {
|
|
284
284
|
// just show the proxy config
|
|
285
285
|
}
|
|
@@ -515,15 +515,22 @@ export class SmartApp extends WebApp {
|
|
|
515
515
|
debug("smartapp", "Received DT value from unit: " + u.getDisplayName() + " -> " + u.value + " / " + u.status + " from unit");
|
|
516
516
|
for (const link of this.links) {
|
|
517
517
|
if (u.isUnit(link.masterAddress, link.masterPort, link.logicalNodeAddress, link.logicalAddress)) {
|
|
518
|
-
const hbValue = dt2hbvalue(u, link.hbValue as number);
|
|
519
|
-
|
|
520
|
-
if (
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
518
|
+
const hbValue = dt2hbvalue(u, link.hbValue as number, link.min, link.max);
|
|
519
|
+
|
|
520
|
+
if (typeof hbValue != "undefined") {
|
|
521
|
+
if (hbValue === -1) {
|
|
522
|
+
log("smartapp", "-> Virtual unit send 'stop' for accessory: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
|
|
523
|
+
const val = await hbStop(this.config.apiHost +":"+ this.config.apiPort, link.accId, link.accService, this.token);
|
|
524
|
+
if (typeof val != "undefined") link.hbValue = val;
|
|
525
|
+
|
|
526
|
+
} else if (hbValue !== link.hbValue) {
|
|
527
|
+
log("smartapp", "-> Setting HB value for accessory: " + link.accName + " to " + hbValue + " of Duotecno unit: " + u.getDisplayName() + " with value " + u.value);
|
|
528
|
+
link.hbValue = hbValue;
|
|
529
|
+
await hbSetState(this.config.apiHost +":"+ this.config.apiPort, link.accId, link.accService, hbValue, this.token);
|
|
530
|
+
|
|
531
|
+
} else {
|
|
532
|
+
debug("smartapp", "-> No change for accessory: " + link.accName + " to " + u.value + " / " + u.status);
|
|
533
|
+
}
|
|
527
534
|
}
|
|
528
535
|
}
|
|
529
536
|
};
|
|
@@ -559,7 +566,7 @@ export class SmartApp extends WebApp {
|
|
|
559
566
|
let message: string;
|
|
560
567
|
try {
|
|
561
568
|
if (context.action === "add") {
|
|
562
|
-
this.links.push(kEmptyLink);
|
|
569
|
+
this.links.push({...kEmptyLink, type: LinkType.kAccessory});
|
|
563
570
|
inx = this.links.length-1;
|
|
564
571
|
return this.ejs("linkDetail", context, { link: this.links[inx], id: inx,
|
|
565
572
|
masters: this.system.masters, accessories, units });
|
|
@@ -594,11 +601,15 @@ export class SmartApp extends WebApp {
|
|
|
594
601
|
const stype = context.getParam({name: "type", type: "string", default: LinkType.kAccessory });
|
|
595
602
|
const name = context.getParam({name: "name", type: "string", default: "--" });
|
|
596
603
|
|
|
604
|
+
// max & min values
|
|
605
|
+
const min = context.getParam({name: "min", type: "integer", default: 0 });
|
|
606
|
+
const max = context.getParam({name: "max", type: "integer", default: 100 });
|
|
607
|
+
|
|
597
608
|
// uniqiue id for the accessory
|
|
598
609
|
const accId = context.getParam({name: "accId", type: "string", default: "" });
|
|
599
610
|
const accName = hbName(accId);
|
|
600
611
|
const accService = hbService(accId);
|
|
601
|
-
return { accId, accName, accService, name, masterAddress, masterPort, logicalAddress, logicalNodeAddress, type: stype }
|
|
612
|
+
return { accId, accName, accService, name, masterAddress, masterPort, logicalAddress, logicalNodeAddress, type: stype, min, max};;
|
|
602
613
|
}
|
|
603
614
|
|
|
604
615
|
updateLink(inx: number, link: Link) {
|
|
@@ -66,8 +66,6 @@
|
|
|
66
66
|
</select>
|
|
67
67
|
<label for="unitSelector">or Pick a unit</label>
|
|
68
68
|
</div>
|
|
69
|
-
|
|
70
|
-
</div>
|
|
71
69
|
</div>
|
|
72
70
|
|
|
73
71
|
<div class="row">
|
|
@@ -89,6 +87,18 @@
|
|
|
89
87
|
</div>
|
|
90
88
|
</div>
|
|
91
89
|
|
|
90
|
+
<div class="row">
|
|
91
|
+
<div class="input-field col s4">
|
|
92
|
+
<label for="min">Off value</label>
|
|
93
|
+
<input placeholder="0" id="min" name="min" type="text" value="<%= link.min %>">
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div class="input-field col s4">
|
|
97
|
+
<label for="max">On value</label>
|
|
98
|
+
<input placeholder="0" id="mx" name="max" type="text" value="<%= link.max %>">
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
92
102
|
</form>
|
|
93
103
|
<%- include('/views/footer'); -%>
|
|
94
104
|
|
package/server/webapp.js
CHANGED
|
@@ -20,7 +20,7 @@ const url = require("url");
|
|
|
20
20
|
const qs = require("querystring");
|
|
21
21
|
const base_1 = require("./base");
|
|
22
22
|
const types = require("../duotecno/types");
|
|
23
|
-
const
|
|
23
|
+
const logger = require("../duotecno/logger");
|
|
24
24
|
const logger_1 = require("../duotecno/logger");
|
|
25
25
|
const child_process_1 = require("child_process");
|
|
26
26
|
;
|
|
@@ -310,9 +310,10 @@ class WebApp extends base_1.Base {
|
|
|
310
310
|
doServe(req, res) {
|
|
311
311
|
return __awaiter(this, void 0, void 0, function* () {
|
|
312
312
|
const context = yield this.parseRequest(req, res);
|
|
313
|
+
let prevRequest = "";
|
|
313
314
|
try {
|
|
314
315
|
if (this.needsLogin(context)) {
|
|
315
|
-
|
|
316
|
+
logger.debug("webapp", "checking ticket: " + context.cookies["DTicket"] + " = " + this.token + " for " + context.request + "/" + context.action + " => " + (context.cookies["DTicket"] == this.token));
|
|
316
317
|
// if a password is set, check if the tickets is valid, if not: redirect to login page
|
|
317
318
|
if (!this.token || (context.cookies["DTicket"] != this.token)) {
|
|
318
319
|
context.request = "login";
|
|
@@ -320,6 +321,10 @@ class WebApp extends base_1.Base {
|
|
|
320
321
|
}
|
|
321
322
|
}
|
|
322
323
|
const httpResult = yield this.doRequest(context);
|
|
324
|
+
// see if we can go to the original request...
|
|
325
|
+
if ((context.request === "login") && (context.path != "/login")) {
|
|
326
|
+
httpResult.header = Object.assign(Object.assign({}, httpResult.header), { "Set-Cookie": "DPath=" + context.path });
|
|
327
|
+
}
|
|
323
328
|
return httpResult;
|
|
324
329
|
}
|
|
325
330
|
catch (e) {
|
|
@@ -393,10 +398,11 @@ class WebApp extends base_1.Base {
|
|
|
393
398
|
const pw = context.getParam({ name: "password", type: "string" });
|
|
394
399
|
const user = context.getParam({ name: "user", type: "string" });
|
|
395
400
|
if (yield this.pwOK(context, user, pw)) {
|
|
396
|
-
|
|
401
|
+
const path = context.getCookie("DPath") || "/";
|
|
402
|
+
return this.redirect(path, { "Set-Cookie": "DTicket=" + this.token });
|
|
397
403
|
}
|
|
398
404
|
else {
|
|
399
|
-
return this.ejs("login", context, { message: "authentication failed" }, { "Set-Cookie": "DTicket=x" });
|
|
405
|
+
return this.ejs("login", context, { message: "authentication failed", user }, { "Set-Cookie": "DTicket=x" });
|
|
400
406
|
}
|
|
401
407
|
});
|
|
402
408
|
}
|
package/server/webapp.ts
CHANGED
|
@@ -9,7 +9,7 @@ import * as url from "url";
|
|
|
9
9
|
import * as qs from "querystring";
|
|
10
10
|
import { Base } from './base';
|
|
11
11
|
import * as types from "../duotecno/types";
|
|
12
|
-
import * as
|
|
12
|
+
import * as logger from "../duotecno/logger";
|
|
13
13
|
import { log } from "../duotecno/logger";
|
|
14
14
|
import { exec } from "child_process";
|
|
15
15
|
|
|
@@ -372,15 +372,20 @@ export class WebApp extends Base {
|
|
|
372
372
|
|
|
373
373
|
async doServe(req: http.IncomingMessage, res: http.ServerResponse): Promise<HttpResponse> {
|
|
374
374
|
const context = await this.parseRequest(req, res);
|
|
375
|
+
let prevRequest = "";
|
|
375
376
|
try {
|
|
376
377
|
if (this.needsLogin(context)) {
|
|
377
|
-
|
|
378
|
+
logger.debug("webapp", "checking ticket: " + context.cookies["DTicket"] + " = " + this.token + " for " + context.request+"/"+context.action + " => " + (context.cookies["DTicket"] == this.token));
|
|
378
379
|
// if a password is set, check if the tickets is valid, if not: redirect to login page
|
|
379
380
|
if (!this.token || (context.cookies["DTicket"] != this.token)) {
|
|
380
381
|
context.request = "login"; context.action = "";
|
|
381
382
|
}
|
|
382
383
|
}
|
|
383
384
|
const httpResult = await this.doRequest(context);
|
|
385
|
+
// see if we can go to the original request...
|
|
386
|
+
if ((context.request === "login") && (context.path != "/login")) {
|
|
387
|
+
httpResult.header = {...httpResult.header, "Set-Cookie": "DPath="+context.path};
|
|
388
|
+
}
|
|
384
389
|
return httpResult;
|
|
385
390
|
|
|
386
391
|
} catch (e) {
|
|
@@ -452,10 +457,11 @@ export class WebApp extends Base {
|
|
|
452
457
|
const user = context.getParam({name: "user", type: "string"});
|
|
453
458
|
|
|
454
459
|
if (await this.pwOK(context, user, pw)) {
|
|
455
|
-
|
|
460
|
+
const path = context.getCookie("DPath") || "/"
|
|
461
|
+
return this.redirect(path, {"Set-Cookie": "DTicket="+this.token});
|
|
456
462
|
|
|
457
463
|
} else {
|
|
458
|
-
return this.ejs("login", context, {message: "authentication failed"}, {"Set-Cookie": "DTicket=x"});
|
|
464
|
+
return this.ejs("login", context, {message: "authentication failed", user}, {"Set-Cookie": "DTicket=x"});
|
|
459
465
|
}
|
|
460
466
|
}
|
|
461
467
|
async doLogout(context: Context): Promise<HttpResponse> {
|