hm-simulator 1.3.2 → 1.5.0
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/CHANGELOG.md +37 -6
- package/README.md +93 -15
- package/data/fixtures/hmip-wired.json +25645 -0
- package/data/paramset-ids.json +1 -0
- package/index.js +5 -4
- package/lib/control.js +3 -0
- package/lib/method-help.js +2 -0
- package/lib/sim.js +263 -4
- package/package.json +1 -1
- package/sim.d.ts +48 -0
package/lib/sim.js
CHANGED
|
@@ -48,7 +48,40 @@ const BIDCOS_INTERFACES = new Set(['rfd', 'wired']);
|
|
|
48
48
|
/** FLAGS bit of a link whose sender rfd counts as broken (bit 2 is the receiver) */
|
|
49
49
|
const LINK_SENDER_BROKEN = 1;
|
|
50
50
|
|
|
51
|
+
/** data/paramset-ids.json, read on the first getParamsetId of a BidCos interface */
|
|
52
|
+
let bundledParamsetIds;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The paramset ids of a BidCos device type and version from the device files
|
|
56
|
+
* (`tools/paramset-ids.js`); a version the files lack takes the highest one below it, else the
|
|
57
|
+
* lowest. `undefined` for a type they do not describe.
|
|
58
|
+
*/
|
|
59
|
+
function bidcosParamsetIds(prefix, type, version) {
|
|
60
|
+
if (!bundledParamsetIds) {
|
|
61
|
+
bundledParamsetIds = require('../data/paramset-ids.json');
|
|
62
|
+
}
|
|
63
|
+
const versions = (bundledParamsetIds[prefix] || {})[type];
|
|
64
|
+
if (!versions) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
if (versions[String(version)]) {
|
|
68
|
+
return versions[String(version)];
|
|
69
|
+
}
|
|
70
|
+
const known = Object.keys(versions)
|
|
71
|
+
.map(Number)
|
|
72
|
+
.sort((a, b) => a - b);
|
|
73
|
+
const below = known.filter((entry) => entry <= Number(version));
|
|
74
|
+
return versions[String(below.length > 0 ? below.at(-1) : known[0])];
|
|
75
|
+
}
|
|
76
|
+
|
|
51
77
|
/** The device part of a channel address: `ABC0000001:1` -> `ABC0000001`. */
|
|
78
|
+
/** An SGTIN or an HmIP device key as the comparisons take it: no dashes or blanks, upper case. */
|
|
79
|
+
function normaliseHex(text) {
|
|
80
|
+
return String(text || '')
|
|
81
|
+
.replace(/[\s-]/g, '')
|
|
82
|
+
.toUpperCase();
|
|
83
|
+
}
|
|
84
|
+
|
|
52
85
|
function deviceOf(address) {
|
|
53
86
|
const colon = String(address).indexOf(':');
|
|
54
87
|
return colon === -1 ? String(address) : String(address).slice(0, colon);
|
|
@@ -66,6 +99,9 @@ const STANDARD_PARAMSETS = new Set(['MASTER', 'VALUES', 'SERVICE']);
|
|
|
66
99
|
/** FLAGS bit of a datapoint that is a service message (1 visible, 2 internal, 4 transform, 8 service) */
|
|
67
100
|
const SERVICE_FLAG = 8;
|
|
68
101
|
|
|
102
|
+
/** FLAGS bit of a datapoint that keeps its value until a client acknowledges it (STICKY_UNREACH) */
|
|
103
|
+
const STICKY_FLAG = 16;
|
|
104
|
+
|
|
69
105
|
/**
|
|
70
106
|
* Per interface behaviour, overridable with the `interfaces` option.
|
|
71
107
|
*
|
|
@@ -96,6 +132,10 @@ const SERVICE_FLAG = 8;
|
|
|
96
132
|
* unreachWrites
|
|
97
133
|
* what a setValue/putParamset to an unreachable device answers: 'accept' (the default) or
|
|
98
134
|
* 'fault' (notReachable). The simulator's model; not measured yet.
|
|
135
|
+
* stickyWriteEvents
|
|
136
|
+
* whether a client's write to a sticky datapoint (FLAGS & 16: the acknowledgement of
|
|
137
|
+
* STICKY_UNREACH) is answered with the channel's events like every other write (true, the
|
|
138
|
+
* default), or with none (false): a registered client on rfd 3.89.11 got no event for it.
|
|
99
139
|
* firmwareUpdateDelay
|
|
100
140
|
* milliseconds per step of a firmware update, see offerFirmware().
|
|
101
141
|
* protocols
|
|
@@ -115,6 +155,7 @@ const INTERFACE_DEFAULTS = {
|
|
|
115
155
|
deliveryTimeout: 10000,
|
|
116
156
|
// device health, see setReachable() and offerFirmware()
|
|
117
157
|
unreachWrites: 'accept',
|
|
158
|
+
stickyWriteEvents: true,
|
|
118
159
|
firmwareUpdateDelay: 0,
|
|
119
160
|
configPendingMode: 'strict',
|
|
120
161
|
configPendingOnWrite: false,
|
|
@@ -210,6 +251,10 @@ function shortenParams(params) {
|
|
|
210
251
|
* @param {boolean} [options.paramsetFallback=true] a device whose firmware has no description uses
|
|
211
252
|
* the nearest firmware of the same type and VERSION; `false` for exact keys only
|
|
212
253
|
* @param {object} [options.faults] overrides for the fault table, see lib/faults.js
|
|
254
|
+
* @param {object} [options.paramsets] stored values to start with,
|
|
255
|
+
* `{<iface>: {<address>: {<MASTER|VALUES|SERVICE|peer address>: {}}}}`
|
|
256
|
+
* @param {object} [options.paramsetIds] what `getParamsetId` answers before the bundled ids,
|
|
257
|
+
* `{<iface>: {<address>: {<paramset>: id}}}`
|
|
213
258
|
*/
|
|
214
259
|
class HmSim {
|
|
215
260
|
constructor(options = {}) {
|
|
@@ -226,6 +271,10 @@ class HmSim {
|
|
|
226
271
|
/** requested key -> the key used for it (a fallback) or null, see resolveParamsetKey() */
|
|
227
272
|
this.paramsetResolved = new Map();
|
|
228
273
|
this.paramsetIndex = new ParamsetIndex(this.paramsetDescriptions);
|
|
274
|
+
/** stored values a paramset starts with, on top of its defaults: {<iface>: {<address>: {<key>: {}}}} */
|
|
275
|
+
this.seededParamsets = structuredClone(options.paramsets || {});
|
|
276
|
+
/** getParamsetId answers given in the options: {<iface>: {<address>: {<paramset>: id}}} */
|
|
277
|
+
this.paramsetIdOverrides = options.paramsetIds || {};
|
|
229
278
|
|
|
230
279
|
const {table, fault} = createFaults(options.faults);
|
|
231
280
|
this.faults = table;
|
|
@@ -285,6 +334,12 @@ class HmSim {
|
|
|
285
334
|
this.keyMismatchScript = {};
|
|
286
335
|
/** the serial getKeyMismatchDevice answers: {<iface>: serial} */
|
|
287
336
|
this.keyMismatch = {};
|
|
337
|
+
/** the whitelist of the open install mode (setInstallModeWithWhitelist): {<iface>: [{ADDRESS, KEY_MODE, KEY}]} */
|
|
338
|
+
this.installWhitelist = {};
|
|
339
|
+
/** HmIP devices in reach of the next install mode: {<iface>: Map<sgtin, {sgtin, key, devices, delay}>}, see scriptInclusion() */
|
|
340
|
+
this.inclusionScripts = {};
|
|
341
|
+
/** every inclusion request a scripted device made: {<iface>: [{sgtin, result, ts}]}, see getInclusions() */
|
|
342
|
+
this.inclusions = {};
|
|
288
343
|
/** suppressed service messages (hmipserver): {<iface>: Map<channelAddress, Set<parameter>>} */
|
|
289
344
|
this.suppressed = {};
|
|
290
345
|
/** metadata per interface and address: {<iface>: {<address>: {<key>: value}}} */
|
|
@@ -362,6 +417,7 @@ class HmSim {
|
|
|
362
417
|
this.newDevicesScript[iface] = {delay: 0, ...this.options.newDevices[iface]};
|
|
363
418
|
}
|
|
364
419
|
this.reindex(iface);
|
|
420
|
+
this.seedValues(iface);
|
|
365
421
|
this.setDefaultValues(iface);
|
|
366
422
|
if (!this.ifaces.includes(iface)) {
|
|
367
423
|
this.ifaces.push(iface);
|
|
@@ -376,6 +432,22 @@ class HmSim {
|
|
|
376
432
|
this.index[iface] = index;
|
|
377
433
|
}
|
|
378
434
|
|
|
435
|
+
/** The VALUES of `options.paramsets`, before setDefaultValues fills in the rest. */
|
|
436
|
+
seedValues(iface) {
|
|
437
|
+
for (const [address, paramsets] of Object.entries(this.seededParamsets[iface] || {})) {
|
|
438
|
+
if (paramsets.VALUES && this.index[iface].has(address)) {
|
|
439
|
+
const state = this.stateOf(iface, address);
|
|
440
|
+
state.VALUES = {...state.VALUES, ...paramsets.VALUES};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** What `options.paramsets` gives a paramset to start with, or `{}`. */
|
|
446
|
+
seededParamset(iface, address, key) {
|
|
447
|
+
const paramsets = this.seededParamsets[iface] && this.seededParamsets[iface][address];
|
|
448
|
+
return (paramsets && paramsets[key]) || {};
|
|
449
|
+
}
|
|
450
|
+
|
|
379
451
|
setDefaultValues(iface) {
|
|
380
452
|
for (const device of this.devices[iface].devices) {
|
|
381
453
|
if (!device.PARENT_TYPE || !device.PARAMSETS || !device.PARAMSETS.includes('VALUES')) {
|
|
@@ -555,14 +627,17 @@ class HmSim {
|
|
|
555
627
|
const description = this.requireParamsetDescription(iface, device, 'LINK');
|
|
556
628
|
state.LINKS = state.LINKS || {};
|
|
557
629
|
if (!state.LINKS[paramset]) {
|
|
558
|
-
state.LINKS[paramset] =
|
|
630
|
+
state.LINKS[paramset] = {
|
|
631
|
+
...defaultParamset(description),
|
|
632
|
+
...this.seededParamset(iface, address, paramset),
|
|
633
|
+
};
|
|
559
634
|
}
|
|
560
635
|
return {device, description, values: state.LINKS[paramset]};
|
|
561
636
|
}
|
|
562
637
|
|
|
563
638
|
const description = this.requireParamsetDescription(iface, device, paramset);
|
|
564
639
|
if (!state[paramset]) {
|
|
565
|
-
state[paramset] = defaultParamset(description);
|
|
640
|
+
state[paramset] = {...defaultParamset(description), ...this.seededParamset(iface, address, paramset)};
|
|
566
641
|
}
|
|
567
642
|
return {device, description, values: state[paramset]};
|
|
568
643
|
}
|
|
@@ -593,6 +668,56 @@ class HmSim {
|
|
|
593
668
|
return this.getParamsetDescription(iface, device, paramset);
|
|
594
669
|
}
|
|
595
670
|
|
|
671
|
+
/**
|
|
672
|
+
* The RPC `getParamsetId(address, paramset)`: the CCU's own name of a paramset, by which its WebUI
|
|
673
|
+
* picks a form (`easymodes/<id>.tcl`). Measured on firmware 3.89.11:
|
|
674
|
+
*
|
|
675
|
+
* - hmipserver builds it: the device type, the channel index and the key, lower case and joined
|
|
676
|
+
* by `_` - `hmipw-drbl4_2_link`, `hmipw-drbl4_master` for the device - for every key it is
|
|
677
|
+
* given, a peer address too (`hmipw-dri16_1_00000000000102:1`), `null` for none.
|
|
678
|
+
* - rfd and hs485d answer the `id` the device file gives the paramset (`cc_tc_ch_wsr_link`,
|
|
679
|
+
* `hmw_input_ch_master`; `data/paramset-ids.json`): MASTER for a device address and MASTER,
|
|
680
|
+
* VALUES, LINK for a channel; any other key on a channel is taken as a peer address and answers
|
|
681
|
+
* the LINK id. A paramset the address does not have is `-3 Unknown paramset`
|
|
682
|
+
* (`unknownParamsetDescription`). A device type the bundled ids lack answers `''`.
|
|
683
|
+
*
|
|
684
|
+
* An unknown address is `unknownInstance` on all of them. `options.paramsetIds` answers first.
|
|
685
|
+
* @param {string} iface
|
|
686
|
+
* @param {Array} params [address, paramset]
|
|
687
|
+
*/
|
|
688
|
+
getParamsetId(iface, params) {
|
|
689
|
+
const [address, key] = params || [];
|
|
690
|
+
const device = this.requireDevice(iface, address);
|
|
691
|
+
const overrides = this.paramsetIdOverrides[iface] && this.paramsetIdOverrides[iface][address];
|
|
692
|
+
if (overrides && typeof key === 'string' && overrides[key] !== undefined) {
|
|
693
|
+
return overrides[key];
|
|
694
|
+
}
|
|
695
|
+
const parent = (device.PARENT ? this.index[iface].get(device.PARENT) : device) || {};
|
|
696
|
+
|
|
697
|
+
if (!BIDCOS_INTERFACES.has(iface)) {
|
|
698
|
+
const name = key === undefined || key === null ? 'null' : String(key).toLowerCase();
|
|
699
|
+
const type = String(parent.TYPE || '').toLowerCase();
|
|
700
|
+
return device.PARENT ? `${type}_${device.INDEX}_${name}` : `${type}_${name}`;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const name = !device.PARENT || ['MASTER', 'VALUES', 'LINK'].includes(key) ? key : 'LINK';
|
|
704
|
+
const has = Array.isArray(device.PARAMSETS) ? device.PARAMSETS.includes(name) : name === 'MASTER';
|
|
705
|
+
if (!has || (!device.PARENT && name !== 'MASTER')) {
|
|
706
|
+
throw this.fault('unknownParamsetDescription', `${address}/${key}`);
|
|
707
|
+
}
|
|
708
|
+
const ids = bidcosParamsetIds(PARAMSET_PREFIX[iface], parent.TYPE, parent.VERSION);
|
|
709
|
+
if (!ids) {
|
|
710
|
+
return '';
|
|
711
|
+
}
|
|
712
|
+
if (!device.PARENT) {
|
|
713
|
+
return ids.device.MASTER || '';
|
|
714
|
+
}
|
|
715
|
+
const channel = ids.channels.find(
|
|
716
|
+
(entry) => device.INDEX >= entry.index && device.INDEX < entry.index + entry.count,
|
|
717
|
+
);
|
|
718
|
+
return (channel && channel[name]) || '';
|
|
719
|
+
}
|
|
720
|
+
|
|
596
721
|
getValue(iface, address, datapoint) {
|
|
597
722
|
const {description, values} = this.storedParamset(iface, address, 'VALUES');
|
|
598
723
|
if (!description[datapoint]) {
|
|
@@ -1190,13 +1315,55 @@ class HmSim {
|
|
|
1190
1315
|
*/
|
|
1191
1316
|
setInstallMode(iface, params) {
|
|
1192
1317
|
const [on, time = 60, mode = 1] = params || [];
|
|
1318
|
+
delete this.installWhitelist[iface];
|
|
1193
1319
|
if (!on) {
|
|
1194
1320
|
this.installMode[iface] = 0;
|
|
1195
1321
|
return '';
|
|
1196
1322
|
}
|
|
1197
1323
|
|
|
1198
|
-
this.installMode[iface] = Date.now() + Number(time) * 1000;
|
|
1199
1324
|
this.log.info(iface, 'install mode on for', time, 'seconds, mode', mode);
|
|
1325
|
+
return this.openInstallMode(iface, time);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/**
|
|
1329
|
+
* `setInstallModeWithWhitelist(on, time, whitelist)` - hmipserver only. Opens the install mode
|
|
1330
|
+
* for the devices of the whitelist, `[{ADDRESS: <SGTIN>, KEY_MODE, KEY}]`: `KEY_MODE: 'LOCAL'`
|
|
1331
|
+
* with the device key as `KEY` (32 hex digits, as the QR code holds it), any other mode without
|
|
1332
|
+
* one (the key server's key). `on: false` closes the install mode. The simulator's model: a
|
|
1333
|
+
* device of scriptInclusion() that is not on the list is not admitted, one with a `LOCAL` key
|
|
1334
|
+
* that is not its own is declined (nothing joins, getInclusions() records it), any other joins.
|
|
1335
|
+
*/
|
|
1336
|
+
setInstallModeWithWhitelist(iface, params) {
|
|
1337
|
+
if (iface !== 'hmip') {
|
|
1338
|
+
throw this.fault('unknownMethod', `${iface}/setInstallModeWithWhitelist`);
|
|
1339
|
+
}
|
|
1340
|
+
const [on, time = 60, whitelist = []] = params || [];
|
|
1341
|
+
if (!on) {
|
|
1342
|
+
delete this.installWhitelist[iface];
|
|
1343
|
+
this.installMode[iface] = 0;
|
|
1344
|
+
return '';
|
|
1345
|
+
}
|
|
1346
|
+
if (
|
|
1347
|
+
!Array.isArray(whitelist) ||
|
|
1348
|
+
!whitelist.every((entry) => entry && typeof entry === 'object' && typeof entry.ADDRESS === 'string')
|
|
1349
|
+
) {
|
|
1350
|
+
throw this.fault(
|
|
1351
|
+
'invalidArguments',
|
|
1352
|
+
`${iface}/setInstallModeWithWhitelist wants [{ADDRESS, KEY_MODE, KEY}]`,
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
this.installWhitelist[iface] = whitelist.map((entry) => ({
|
|
1356
|
+
ADDRESS: normaliseHex(entry.ADDRESS),
|
|
1357
|
+
KEY_MODE: typeof entry.KEY_MODE === 'string' ? entry.KEY_MODE : '',
|
|
1358
|
+
KEY: typeof entry.KEY === 'string' ? entry.KEY : '',
|
|
1359
|
+
}));
|
|
1360
|
+
this.log.info(iface, 'install mode on for', time, 'seconds, whitelist of', whitelist.length);
|
|
1361
|
+
return this.openInstallMode(iface, time);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/** The install mode opens: the scripted devices come into reach. */
|
|
1365
|
+
openInstallMode(iface, time) {
|
|
1366
|
+
this.installMode[iface] = Date.now() + Number(time) * 1000;
|
|
1200
1367
|
|
|
1201
1368
|
const scripted = this.newDevicesScript[iface];
|
|
1202
1369
|
if (scripted) {
|
|
@@ -1209,9 +1376,95 @@ class HmSim {
|
|
|
1209
1376
|
this.timer(() => this.hearKeyMismatchDevice(iface, mismatch), mismatch.delay);
|
|
1210
1377
|
}
|
|
1211
1378
|
|
|
1379
|
+
for (const script of (this.inclusionScripts[iface] || new Map()).values()) {
|
|
1380
|
+
this.timer(() => this.hearInclusion(iface, script), script.delay);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1212
1383
|
return '';
|
|
1213
1384
|
}
|
|
1214
1385
|
|
|
1386
|
+
/* ------------------------------------------------------------------ HmIP inclusion */
|
|
1387
|
+
|
|
1388
|
+
/**
|
|
1389
|
+
* An HmIP device in factory state comes into reach of the next install mode and asks to join
|
|
1390
|
+
* (Homematic Manager task 88). `key` is its device key as the QR code holds it (32 hex digits;
|
|
1391
|
+
* case and dashes do not matter). Opened with setInstallMode, it joins (the key server knows its
|
|
1392
|
+
* key); opened with setInstallModeWithWhitelist, it joins when it is on the list and its entry's
|
|
1393
|
+
* `LOCAL` key is `key` or the entry asks the key server, is declined with another `LOCAL` key,
|
|
1394
|
+
* and is not admitted when it is not on the list. Declined and not admitted, it stays in reach
|
|
1395
|
+
* for the next install mode; joined, `devices` go out with `newDevices` and the script is used
|
|
1396
|
+
* up. Each request is recorded (getInclusions()).
|
|
1397
|
+
*
|
|
1398
|
+
* @param {string} iface
|
|
1399
|
+
* @param {string} sgtin
|
|
1400
|
+
* @param {object} [script]
|
|
1401
|
+
* @param {string} [script.key=''] its device key
|
|
1402
|
+
* @param {Array} [script.devices=[]] its description and channel descriptions
|
|
1403
|
+
* @param {number} [script.delay=0] milliseconds between the install mode and the request
|
|
1404
|
+
*/
|
|
1405
|
+
scriptInclusion(iface, sgtin, {key = '', devices = [], delay = 0} = {}) {
|
|
1406
|
+
if (typeof sgtin !== 'string' || sgtin === '') {
|
|
1407
|
+
throw new TypeError('scriptInclusion wants an SGTIN');
|
|
1408
|
+
}
|
|
1409
|
+
if (!this.inclusionScripts[iface]) {
|
|
1410
|
+
this.inclusionScripts[iface] = new Map();
|
|
1411
|
+
}
|
|
1412
|
+
const normalised = normaliseHex(sgtin);
|
|
1413
|
+
this.inclusionScripts[iface].set(normalised, {
|
|
1414
|
+
sgtin: normalised,
|
|
1415
|
+
key: normaliseHex(key),
|
|
1416
|
+
devices: devices || [],
|
|
1417
|
+
delay,
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/** The scripted device asks to join: joined, declined for its key, or not admitted. */
|
|
1422
|
+
hearInclusion(iface, script) {
|
|
1423
|
+
const scripts = this.inclusionScripts[iface];
|
|
1424
|
+
if (!scripts || scripts.get(script.sgtin) !== script || this.getInstallMode(iface) === 0) {
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
let result = 'paired';
|
|
1428
|
+
const whitelist = this.installWhitelist[iface];
|
|
1429
|
+
if (whitelist) {
|
|
1430
|
+
const entry = whitelist.find((candidate) => candidate.ADDRESS === script.sgtin);
|
|
1431
|
+
if (!entry) {
|
|
1432
|
+
result = 'ignored';
|
|
1433
|
+
} else if (entry.KEY_MODE === 'LOCAL' && normaliseHex(entry.KEY) !== script.key) {
|
|
1434
|
+
result = 'declined';
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
if (!this.inclusions[iface]) {
|
|
1438
|
+
this.inclusions[iface] = [];
|
|
1439
|
+
}
|
|
1440
|
+
this.inclusions[iface].push({sgtin: script.sgtin, result, ts: Date.now()});
|
|
1441
|
+
this.log.info(
|
|
1442
|
+
iface,
|
|
1443
|
+
script.sgtin,
|
|
1444
|
+
result === 'paired'
|
|
1445
|
+
? 'joined'
|
|
1446
|
+
: result === 'declined'
|
|
1447
|
+
? 'declined, the key does not match'
|
|
1448
|
+
: 'not on the whitelist',
|
|
1449
|
+
);
|
|
1450
|
+
if (result === 'paired') {
|
|
1451
|
+
scripts.delete(script.sgtin);
|
|
1452
|
+
if (script.devices.length > 0) {
|
|
1453
|
+
this.addDevices(iface, script.devices);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/** Every inclusion request of a scripted device, oldest first: `[{sgtin, result, ts}]`, result `paired`, `declined` or `ignored`. */
|
|
1459
|
+
getInclusions(iface) {
|
|
1460
|
+
return [...(this.inclusions[iface] || [])];
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/** The whitelist of the open install mode, `[]` when it was opened without one or is closed. */
|
|
1464
|
+
getInstallWhitelist(iface) {
|
|
1465
|
+
return this.getInstallMode(iface) === 0 ? [] : structuredClone(this.installWhitelist[iface] || []);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1215
1468
|
/* ------------------------------------------------------------------ key mismatch */
|
|
1216
1469
|
|
|
1217
1470
|
/**
|
|
@@ -2465,6 +2718,7 @@ class HmSim {
|
|
|
2465
2718
|
},
|
|
2466
2719
|
getValue: (iface, params) => this.getValue(iface, params[0], params[1]),
|
|
2467
2720
|
getParamset: (iface, params) => this.getParamset(iface, params[0], params[1]),
|
|
2721
|
+
getParamsetId: (iface, params) => this.getParamsetId(iface, params),
|
|
2468
2722
|
putParamset: (iface, params) => {
|
|
2469
2723
|
this.requireReachable(iface, params[0]);
|
|
2470
2724
|
return this.putParamset(iface, params[0], params[1], params[2]);
|
|
@@ -2482,6 +2736,7 @@ class HmSim {
|
|
|
2482
2736
|
setBidcosInterface: (iface, params) => this.setBidcosInterface(iface, params),
|
|
2483
2737
|
getServiceMessages: (iface) => this.getServiceMessages(iface),
|
|
2484
2738
|
setInstallMode: (iface, params) => this.setInstallMode(iface, params),
|
|
2739
|
+
setInstallModeWithWhitelist: (iface, params) => this.setInstallModeWithWhitelist(iface, params),
|
|
2485
2740
|
getInstallMode: (iface) => this.getInstallMode(iface),
|
|
2486
2741
|
setTempKey: (iface, params) => this.setTempKey(iface, params),
|
|
2487
2742
|
listTeams: (iface) => this.listTeams(iface),
|
|
@@ -2973,7 +3228,11 @@ class HmSim {
|
|
|
2973
3228
|
const state = this.stateOf(iface, address);
|
|
2974
3229
|
state.VALUES[datapoint] = casted;
|
|
2975
3230
|
|
|
2976
|
-
|
|
3231
|
+
// a client's acknowledgement of a sticky datapoint: the value goes, and with
|
|
3232
|
+
// stickyWriteEvents false nobody is told (a strict write is one that came over RPC)
|
|
3233
|
+
const silent = strict && parameter.FLAGS & STICKY_FLAG && !this.interfaceOption(iface, 'stickyWriteEvents');
|
|
3234
|
+
|
|
3235
|
+
if (parameter.OPERATIONS & 4 && !silent) {
|
|
2977
3236
|
const events =
|
|
2978
3237
|
parameter.TYPE === 'ACTION'
|
|
2979
3238
|
? [[address, datapoint, casted]]
|
package/package.json
CHANGED
package/sim.d.ts
CHANGED
|
@@ -142,6 +142,8 @@ declare namespace HmSim {
|
|
|
142
142
|
pingDelay?: number;
|
|
143
143
|
startDelay?: number;
|
|
144
144
|
unreachWrites?: 'accept' | 'fault';
|
|
145
|
+
/** `false`: a client's write to a sticky datapoint (FLAGS & 16) sends no event, as observed on rfd */
|
|
146
|
+
stickyWriteEvents?: boolean;
|
|
145
147
|
firmwareUpdateDelay?: number;
|
|
146
148
|
/** what `getVersion` answers */
|
|
147
149
|
version?: string;
|
|
@@ -183,6 +185,13 @@ declare namespace HmSim {
|
|
|
183
185
|
defaultRssi?: number;
|
|
184
186
|
/** `{<iface>: {<address>: {<key>: value}}}`, what `getMetadata` answers before any `setMetadata` */
|
|
185
187
|
metadata?: Record<string, Record<string, Record<string, unknown>>>;
|
|
188
|
+
/**
|
|
189
|
+
* `{<iface>: {<address>: {<MASTER|VALUES|SERVICE|peer address>: {<parameter>: value}}}}`: stored
|
|
190
|
+
* values a paramset starts with, on top of its description's defaults
|
|
191
|
+
*/
|
|
192
|
+
paramsets?: Record<string, Record<string, Record<string, Record<string, unknown>>>>;
|
|
193
|
+
/** `{<iface>: {<address>: {<paramset>: id}}}`, what `getParamsetId` answers before the bundled ids */
|
|
194
|
+
paramsetIds?: Record<string, Record<string, Record<string, string>>>;
|
|
186
195
|
tls?: TlsOption;
|
|
187
196
|
auth?: BasicAuth;
|
|
188
197
|
}
|
|
@@ -238,6 +247,31 @@ declare namespace HmSim {
|
|
|
238
247
|
args?: unknown[];
|
|
239
248
|
}
|
|
240
249
|
|
|
250
|
+
/** scriptInclusion(): an HmIP device in reach of the next install mode */
|
|
251
|
+
interface InclusionScript {
|
|
252
|
+
/** its device key as the QR code holds it (32 hex digits) */
|
|
253
|
+
key?: string;
|
|
254
|
+
/** its description and channel descriptions, paired when it joins */
|
|
255
|
+
devices?: DeviceDescription[];
|
|
256
|
+
/** milliseconds between the install mode and its request */
|
|
257
|
+
delay?: number;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** One inclusion request of a scripted HmIP device. */
|
|
261
|
+
interface Inclusion {
|
|
262
|
+
sgtin: string;
|
|
263
|
+
/** joined; declined because the whitelist's LOCAL key is not its key; not on the whitelist */
|
|
264
|
+
result: 'paired' | 'declined' | 'ignored';
|
|
265
|
+
ts: number;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** One entry of setInstallModeWithWhitelist's list, as the simulator keeps it. */
|
|
269
|
+
interface WhitelistEntry {
|
|
270
|
+
ADDRESS: string;
|
|
271
|
+
KEY_MODE: string;
|
|
272
|
+
KEY: string;
|
|
273
|
+
}
|
|
274
|
+
|
|
241
275
|
interface KeyMismatchScript {
|
|
242
276
|
/** the passphrase the device was taught in with */
|
|
243
277
|
key?: string;
|
|
@@ -349,6 +383,12 @@ declare class HmSim {
|
|
|
349
383
|
keyMismatch: Record<string, string>;
|
|
350
384
|
/** what scriptKeyMismatch() set up and did not pair yet */
|
|
351
385
|
keyMismatchScript: Record<string, Required<HmSim.KeyMismatchScript> & {serial: string}>;
|
|
386
|
+
/** the whitelist the install mode was last opened with (setInstallModeWithWhitelist), per interface */
|
|
387
|
+
installWhitelist: Record<string, HmSim.WhitelistEntry[]>;
|
|
388
|
+
/** the HmIP devices scriptInclusion() put in reach and that did not join yet, per interface and SGTIN */
|
|
389
|
+
inclusionScripts: Record<string, Map<string, Required<HmSim.InclusionScript> & {sgtin: string}>>;
|
|
390
|
+
/** every inclusion request of a scripted device, per interface */
|
|
391
|
+
inclusions: Record<string, HmSim.Inclusion[]>;
|
|
352
392
|
/** the temporary key per interface, as setTempKey left it */
|
|
353
393
|
tempKey: Record<string, string>;
|
|
354
394
|
/** the log level per interface, as logLevel set it */
|
|
@@ -381,6 +421,12 @@ declare class HmSim {
|
|
|
381
421
|
scriptNewDevices(iface: string, devices: HmSim.DeviceDescription[], delay?: number): void;
|
|
382
422
|
/** A device that holds another system's key: heard at the next install mode, paired with its key. */
|
|
383
423
|
scriptKeyMismatch(iface: string, serial: string, script?: HmSim.KeyMismatchScript): void;
|
|
424
|
+
/** An HmIP device in reach of the next install mode: joins, or is declined for a whitelist key that is not its own. */
|
|
425
|
+
scriptInclusion(iface: string, sgtin: string, script?: HmSim.InclusionScript): void;
|
|
426
|
+
/** Every inclusion request of a scripted device, oldest first. */
|
|
427
|
+
getInclusions(iface: string): HmSim.Inclusion[];
|
|
428
|
+
/** The whitelist of the open install mode, `[]` without one. */
|
|
429
|
+
getInstallWhitelist(iface: string): HmSim.WhitelistEntry[];
|
|
384
430
|
|
|
385
431
|
/* ---------------------------------------------------------------- scenario api: values and events */
|
|
386
432
|
|
|
@@ -463,6 +509,8 @@ declare class HmSim {
|
|
|
463
509
|
): void;
|
|
464
510
|
getLinks(iface: string, params?: unknown[]): Array<HmSim.Link & Record<string, unknown>>;
|
|
465
511
|
getLinkPeers(iface: string, params: unknown[]): string[];
|
|
512
|
+
/** `getParamsetId(address, paramset)`: hmipserver's built id, or the BidCos device file's */
|
|
513
|
+
getParamsetId(iface: string, params: [address: string, paramset?: string]): string;
|
|
466
514
|
deleteDevice(iface: string, address: string, flags?: number): '';
|
|
467
515
|
replaceDevice(iface: string, oldAddress: string, newAddress: string): true;
|
|
468
516
|
getKeyMismatchDevice(iface: string, params?: [reset?: boolean]): string;
|