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/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] = defaultParamset(description);
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
- if (parameter.OPERATIONS & 4) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hm-simulator",
3
- "version": "1.3.2",
3
+ "version": "1.5.0",
4
4
  "description": "Simulates a Homematic CCU (rfd, hmipserver, VirtualDevices, CUxD, ReGa) for automated tests",
5
5
  "main": "sim.js",
6
6
  "types": "./sim.d.ts",
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;