hm-simulator 1.3.1 → 1.4.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 CHANGED
@@ -3,6 +3,30 @@
3
3
  All notable changes of hm-simulator. The release workflow takes each version's section from here for the
4
4
  GitHub release notes.
5
5
 
6
+ ## 1.4.0 — 2026-10-03
7
+
8
+ - `setInstallModeWithWhitelist(on, time, [{ADDRESS, KEY_MODE, KEY}])` on hmipserver, which answered
9
+ the unknown-method fault: it opens the install mode for the listed SGTINs (Homematic Manager's
10
+ pairing with the key from the sticker), `on: false` closes it. The scenario call `scriptInclusion`
11
+ puts an HmIP device in reach of the next install mode: it joins, or - with a whitelist `LOCAL` key
12
+ that is not its own - is declined, nothing joins and the device stays in reach; `getInclusions`
13
+ answers every request with its result (`paired`, `declined`, `ignored`), `getInstallWhitelist` the
14
+ open install mode's list. The simulator's model, not a measurement (Homematic Manager task 88).
15
+
16
+ ## 1.3.2
17
+
18
+ - `getParamsetDescription` of an address the interface does not know answers the `unknownInstance`
19
+ fault - `-2 Invalid device` under hmipserver's default table, `-2 Unknown instance` under rfd's -
20
+ as both interface processes do on 3.89.11; it answered `''`. A paramset the channel does not list
21
+ answers the new table entry `unknownParamsetDescription` (`-3 Unknown Paramset` / `-3 Unknown
22
+ paramset`, measured); a paramset the channel has but the description set lacks still answers `''`
23
+ (B-5, found through node-red-contrib-ccu's handling of the fault on a real CCU).
24
+ - `getLinkPeers` of a device address faults on `rfd` with the `notSupported` entry (`-1 Failure`
25
+ under rfd's table, as rfd 3.89.11 answers; `-1 Generic error` under the default one), where it
26
+ answered the peers of all channels; on hmipserver and the other interfaces it answers the channels'
27
+ lists one after the other, a peer linked with two of the channels twice, as hmipserver 3.89.11 does
28
+ (B-4).
29
+
6
30
  ## 1.3.1
7
31
 
8
32
  - `getLinks` without an address reports `FLAGS: 1` (`SENDER_BROKEN`) on every device-internal link of
package/README.md CHANGED
@@ -122,17 +122,17 @@ that starts with the bytes `Bin` is BIN-RPC, anything else XML-RPC over HTTP.
122
122
 
123
123
  **Incoming RPC methods.**
124
124
 
125
- | | |
126
- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
127
- | session | `init` (register and de-register a logic layer), `ping`, `system.listMethods`, `system.methodHelp`, `system.multicall` |
128
- | devices | `listDevices`, `getDeviceDescription`, `deleteDevice`, `replaceDevice`, `listReplaceableDevices`, `setInstallMode`, `getInstallMode`, `addDevice` (BidCos only), `getKeyMismatchDevice` (rfd, hmipserver) |
129
- | paramsets | `getParamsetDescription`, `getParamset`, `putParamset`, `getValue`, `setValue`, `determineParameter`, `reportValueUsage` |
130
- | links | `getLinks`, `getLinkPeers`, `getLinkInfo`, `setLinkInfo`, `addLink`, `removeLink`, `activateLinkParamset` |
131
- | interface | `rssiInfo`, `listBidcosInterfaces`, `setBidcosInterface`, `getServiceMessages`, `setTempKey` (BidCos only), `changeKey` (rfd, hmipserver), `logLevel`, `getVersion`, `setInterfaceClock` (rfd, hmipserver), `getLGWStatus` (only when configured) |
132
- | metadata | `getMetadata`, `setMetadata` (rfd, hs485d, hmipserver), `getAllMetadata` (BidCos only) |
133
- | service messages | `suppressServiceMessages`, `getSuppressedServiceMessages` (hmipserver only) |
134
- | teams | `listTeams`, `setTeam` (BidCos only): the smoke detector teams as rfd keeps them, pseudo devices `*<serial>` |
135
- | maintenance | `clearConfigCache`, `restoreConfigToDevice`, `updateFirmware`, `installFirmware`, `refreshDeployedDeviceFirmwareList` (rfd, hmipserver) |
125
+ | | |
126
+ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
127
+ | session | `init` (register and de-register a logic layer), `ping`, `system.listMethods`, `system.methodHelp`, `system.multicall` |
128
+ | devices | `listDevices`, `getDeviceDescription`, `deleteDevice`, `replaceDevice`, `listReplaceableDevices`, `setInstallMode`, `setInstallModeWithWhitelist` (hmipserver), `getInstallMode`, `addDevice` (BidCos only), `getKeyMismatchDevice` (rfd, hmipserver) |
129
+ | paramsets | `getParamsetDescription`, `getParamset`, `putParamset`, `getValue`, `setValue`, `determineParameter`, `reportValueUsage` |
130
+ | links | `getLinks`, `getLinkPeers`, `getLinkInfo`, `setLinkInfo`, `addLink`, `removeLink`, `activateLinkParamset` |
131
+ | interface | `rssiInfo`, `listBidcosInterfaces`, `setBidcosInterface`, `getServiceMessages`, `setTempKey` (BidCos only), `changeKey` (rfd, hmipserver), `logLevel`, `getVersion`, `setInterfaceClock` (rfd, hmipserver), `getLGWStatus` (only when configured) |
132
+ | metadata | `getMetadata`, `setMetadata` (rfd, hs485d, hmipserver), `getAllMetadata` (BidCos only) |
133
+ | service messages | `suppressServiceMessages`, `getSuppressedServiceMessages` (hmipserver only) |
134
+ | teams | `listTeams`, `setTeam` (BidCos only): the smoke detector teams as rfd keeps them, pseudo devices `*<serial>` |
135
+ | maintenance | `clearConfigCache`, `restoreConfigToDevice`, `updateFirmware`, `installFirmware`, `refreshDeployedDeviceFirmwareList` (rfd, hmipserver) |
136
136
 
137
137
  Every write is checked against the paramset description (type, range, `VALUE_LIST`,
138
138
  `OPERATIONS`) and answered the way the interface process answers it - see
@@ -246,6 +246,14 @@ sim.getDevice('rfd', 'LAB0000002:1'); // the description, false when unknown
246
246
  // a device that holds another system's key: the next install mode hears it (getKeyMismatchDevice
247
247
  // names it) until setTempKey('their-key') lets it pair; addDevice('LAB0000009') does the same
248
248
  sim.scriptKeyMismatch('rfd', 'LAB0000009', {key: 'their-key', devices: [device, channel0, channel1]});
249
+ // an HmIP device in factory state asks the next install mode to join; with a whitelist whose LOCAL
250
+ // key is not its key it is declined, and stays in reach for the next one
251
+ sim.scriptInclusion('hmip', '3014F711A000000000000088', {
252
+ key: '00112233445566778899AABBCCDDEEFF',
253
+ devices: [hmipDevice],
254
+ });
255
+ sim.getInclusions('hmip'); // [{sgtin, result: 'paired' | 'declined' | 'ignored', ts}]
256
+ sim.getInstallWhitelist('hmip'); // the open install mode's [{ADDRESS, KEY_MODE, KEY}], [] without one
249
257
  ```
250
258
 
251
259
  **Values and events**
@@ -496,8 +504,13 @@ information there. Asked for one address, `getLinks("ABC0000001:1", 0)` and `get
496
504
  0)` answer the same link with `FLAGS: 0`, and `getLinkPeers` names the own channel once. The simulator
497
505
  does the same on `rfd`: `addLink` stores `FLAGS: 0`, the unfiltered list adds bit 1 to internal links
498
506
  (seeded flags are kept and or-ed), the filtered one reports what is stored. hmipserver is not
499
- measured and reports the stored flags. rfd also refuses `getLinkPeers` of a device address (`-1
500
- Failure`); the simulator answers the peers of all its channels.
507
+ measured and reports the stored flags.
508
+
509
+ `getLinkPeers` of a **device** address (measured 2026-10-02, 3.89.11): rfd refuses it with `-1
510
+ Failure`; hmipserver answers the channels' peer lists one after the other, so a peer linked with two
511
+ of the channels is named twice. The simulator does the same: on `rfd` the `notSupported` entry of the
512
+ fault table in force (`-1 Generic error` under hmipserver's default table, `-1 Failure` under
513
+ `FAULT_TABLES.bidcos`), on every other interface the channels' lists in channel order.
501
514
 
502
515
  ### CONFIG_PENDING
503
516
 
@@ -535,20 +548,21 @@ The two models are the explanations that competed before the measurement (Homema
535
548
  The default table is **hmipserver's**, because that is the one an application has to survive
536
549
  (measured):
537
550
 
538
- | key | code | string | when |
539
- | ------------------ | ---- | ------------------------------- | ---------------------------------------------------------------------------------- |
540
- | `unknownMethod` | -1 | Invalid XML-RPC message | no handler for the method name (hmipserver answers this without a faultCode) |
541
- | `unknownInstance` | -2 | Invalid device | address not known to this interface |
542
- | `unknownParamset` | -2 | Invalid device | the device has no such paramset |
543
- | `unknownLink` | -2 | Invalid device | the two channels are not linked |
544
- | `unknownParameter` | -5 | Unknown Parameter for value key | the paramset has no such parameter |
545
- | `readOnly` | -5 | Invalid parameter or value | `OPERATIONS & 2` is not set |
546
- | `typeError` | -5 | Invalid parameter or value | value does not fit the parameter's `TYPE` |
547
- | `outOfRange` | -5 | Invalid parameter or value | ENUM value not in `VALUE_LIST` |
548
- | `invalidValue` | -5 | Invalid parameter or value | the stored channel configuration cannot be transferred |
549
- | `notSupported` | -1 | Generic error | a method this interface does not implement |
550
- | `notReachable` | -1 | Generic error (UNREACH) | a sleeping battery device |
551
- | `invalidArguments` | -321 | Invalid arguments | wrong number of arguments; hmipserver really answers a Java exception message here |
551
+ | key | code | string | when |
552
+ | ---------------------------- | ---- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
553
+ | `unknownMethod` | -1 | Invalid XML-RPC message | no handler for the method name (hmipserver answers this without a faultCode) |
554
+ | `unknownInstance` | -2 | Invalid device | address not known to this interface |
555
+ | `unknownParamset` | -2 | Invalid device | `getParamset`/`putParamset` of a paramset the device does not have |
556
+ | `unknownParamsetDescription` | -3 | Unknown Paramset | `getParamsetDescription` of a paramset the channel does not list (3.89.11 appends the name: `Unknown Paramset: NOSUCH`) |
557
+ | `unknownLink` | -2 | Invalid device | the two channels are not linked |
558
+ | `unknownParameter` | -5 | Unknown Parameter for value key | the paramset has no such parameter |
559
+ | `readOnly` | -5 | Invalid parameter or value | `OPERATIONS & 2` is not set |
560
+ | `typeError` | -5 | Invalid parameter or value | value does not fit the parameter's `TYPE` |
561
+ | `outOfRange` | -5 | Invalid parameter or value | ENUM value not in `VALUE_LIST` |
562
+ | `invalidValue` | -5 | Invalid parameter or value | the stored channel configuration cannot be transferred |
563
+ | `notSupported` | -1 | Generic error | a method this interface does not implement |
564
+ | `notReachable` | -1 | Generic error (UNREACH) | a sleeping battery device |
565
+ | `invalidArguments` | -321 | Invalid arguments | wrong number of arguments; hmipserver really answers a Java exception message here |
552
566
 
553
567
  Over XML-RPC a fault is an XML-RPC fault, over BIN-RPC a message of type `0xff` with a
554
568
  `faultCode`/`faultString` struct. rfd's table is exported as `BIDCOS_FAULTS` and differs in more
@@ -561,6 +575,22 @@ new HmSim({faults: FAULT_TABLES.bidcos});
561
575
  new HmSim({faults: {unknownParameter: {faultCode: -4, faultString: 'Unknown parameter'}}}); // one entry
562
576
  ```
563
577
 
578
+ ### Unknown addresses and paramsets
579
+
580
+ Measured 2026-10-02 on rfd and hmipserver of firmware 3.89.11, with a device address and a channel
581
+ address alike: an address the interface does not know is a fault from every method that takes one -
582
+ `getDeviceDescription`, `getParamsetDescription`, `getParamset`, `getLinks`, `getLinkPeers` - rfd's
583
+ `-2 Unknown instance`, hmipserver's `-2 Invalid device`. The simulator answers `unknownInstance` of
584
+ the table in force everywhere (until 1.3.1 `getParamsetDescription` answered `''` there).
585
+
586
+ `getParamsetDescription` of a paramset the channel does not have: hmipserver answers `-3 Unknown
587
+ Paramset: <name>`; rfd answers `-3 Unknown paramset` on a channel without a `LINK` paramset and the
588
+ `LINK` description on a channel that has one (the name is taken as a peer address, as `getParamset`
589
+ does). The simulator answers `unknownParamsetDescription` whenever the channel's `PARAMSETS` lacks
590
+ the name. A paramset the channel lists but the description set has no entry for still answers `''`:
591
+ that is a gap in the fixture (logged once, see
592
+ [Device data and fixtures](#device-data-and-fixtures)), not something an interface process does.
593
+
564
594
  ### Service messages
565
595
 
566
596
  `getServiceMessages` answers `[[address, datapoint, value]]`: every datapoint whose description
@@ -627,6 +657,17 @@ knows only its own addresses; `setInterfaceClock` is recorded, not measured (it
627
657
  devices get). `addDevice(serial)` pairs only the device of `scriptKeyMismatch`, with the right
628
658
  temporary key; any other serial is `unknownInstance`, the mismatch a `notSupported` fault.
629
659
 
660
+ `setInstallModeWithWhitelist(on, time, [{ADDRESS, KEY_MODE, KEY}])` (hmipserver only; the others
661
+ answer the unknown-method fault) opens the install mode for the listed SGTINs, as Homematic Manager
662
+ sends it for a device paired with the key from its sticker (`KEY_MODE: 'LOCAL'`, `KEY` the 32 hex
663
+ digits of the QR code), and `on: false` closes it; a list that is not an array of structs with an
664
+ `ADDRESS` is `invalidArguments`. Which devices it admits is the simulator's model, not a
665
+ measurement: a device of `scriptInclusion` joins when it is on the list and the entry's `LOCAL` key
666
+ is its own (or the entry names another key mode, the key server's), is **declined** when the `LOCAL`
667
+ key is another - nothing joins, no RPC client is told, `getInclusions` records it, the device stays
668
+ in reach - and is not admitted (`ignored`) when it is not on the list. Opened with `setInstallMode`,
669
+ the device joins.
670
+
630
671
  ## ReGa mock
631
672
 
632
673
  `rega: {port: 8181, ...}` starts an HTTP server that answers `POST /<name>.exe` as the CCU's
@@ -738,7 +779,8 @@ there. The flags win over the file:
738
779
  arguments. The answer is `{"result": ...}`; a fault is `400` with `faultCode` and `faultString`, an
739
780
  unknown call `404`. `GET /ports` answers the ports. The calls: `addDevice`, `removeDevice`,
740
781
  `fireEvent`, `fireEvents`, `setValue` (the device reporting a value, as a behaviour script does),
741
- `setServiceMessage`, `scriptNewDevices`, `scriptKeyMismatch`, `dropConnection`, `stopInterface`, `startInterface`,
782
+ `setServiceMessage`, `scriptNewDevices`, `scriptKeyMismatch`, `scriptInclusion`, `getInclusions`,
783
+ `getInstallWhitelist`, `dropConnection`, `stopInterface`, `startInterface`,
742
784
  `restartInterface`, `injectFault`, `clearFaults`, `setReachable`, `setLowBattery`, `setDutyCycle`,
743
785
  `setCarrierSense`, `offerFirmware`, `schedule`, `getDevice`, `getWriteLog`, `getConfigPending`,
744
786
  `getPoisonedChannels`, `getMissingParamsetDescriptions`, `getCallbackLog`, `getTempKey`,
package/lib/control.js CHANGED
@@ -15,6 +15,9 @@ const SCENARIO_METHODS = new Set([
15
15
  'setServiceMessage',
16
16
  'scriptNewDevices',
17
17
  'scriptKeyMismatch',
18
+ 'scriptInclusion',
19
+ 'getInclusions',
20
+ 'getInstallWhitelist',
18
21
  'dropConnection',
19
22
  'stopInterface',
20
23
  'startInterface',
package/lib/faults.d.ts CHANGED
@@ -11,6 +11,7 @@ export type FaultName =
11
11
  | 'unknownMethod'
12
12
  | 'unknownInstance'
13
13
  | 'unknownParamset'
14
+ | 'unknownParamsetDescription'
14
15
  | 'unknownParameter'
15
16
  | 'unknownLink'
16
17
  | 'readOnly'
package/lib/faults.js CHANGED
@@ -35,8 +35,11 @@ const DEFAULT_FAULTS = {
35
35
  unknownMethod: {faultCode: -1, faultString: 'Invalid XML-RPC message'},
36
36
  // device or channel address not known to this interface process
37
37
  unknownInstance: {faultCode: -2, faultString: 'Invalid device'},
38
- // the device/channel exists but does not have the requested paramset
38
+ // the device/channel exists but does not have the requested paramset (getParamset, putParamset)
39
39
  unknownParamset: {faultCode: -2, faultString: 'Invalid device'},
40
+ // getParamsetDescription of a paramset the channel does not have. hmipserver 3.89.11 appends the
41
+ // name it was asked for (`Unknown Paramset: NOSUCH`); measured 2026-10-02
42
+ unknownParamsetDescription: {faultCode: -3, faultString: 'Unknown Paramset'},
40
43
  // the paramset exists but does not contain the requested parameter (getValue/setValue)
41
44
  unknownParameter: {faultCode: -5, faultString: 'Unknown Parameter for value key'},
42
45
  // the two channels are not linked with each other
@@ -68,6 +71,10 @@ const BIDCOS_FAULTS = {
68
71
  // rfd does not fault here at all: an unknown paramset name is taken as a peer channel address
69
72
  // and the link defaults come back. The entry covers what the simulator cannot model.
70
73
  unknownParamset: {faultCode: -2, faultString: 'Unknown instance'},
74
+ // getParamsetDescription of a paramset the channel does not have, on a channel without a LINK
75
+ // paramset; a channel that has one answers the LINK description for any unknown name (rfd
76
+ // 3.89.11, measured 2026-10-02)
77
+ unknownParamsetDescription: {faultCode: -3, faultString: 'Unknown paramset'},
71
78
  unknownParameter: {faultCode: -5, faultString: 'Unknown parameter'},
72
79
  unknownLink: {faultCode: -1, faultString: 'Failure'},
73
80
  // rfd accepts a setValue on a read-only datapoint, so this is never raised against it
@@ -40,6 +40,8 @@ module.exports = {
40
40
  getServiceMessages: 'array getServiceMessages() - the pending service messages',
41
41
  setInstallMode: 'void setInstallMode(boolean on, int time, int mode) - switch the install mode',
42
42
  getInstallMode: 'int getInstallMode() - remaining seconds of the install mode',
43
+ setInstallModeWithWhitelist:
44
+ 'void setInstallModeWithWhitelist(boolean on, int time, array whitelist) - the install mode for [{ADDRESS, KEY_MODE, KEY}] only; HmIP only',
43
45
  setTempKey: 'void setTempKey(string passphrase) - the temporary key the next BidCos pairings use; BidCos only',
44
46
  listTeams: 'array listTeams() - the team pseudo devices (smoke detector teams) with their channels; BidCos only',
45
47
  setTeam:
package/lib/sim.js CHANGED
@@ -49,6 +49,13 @@ const BIDCOS_INTERFACES = new Set(['rfd', 'wired']);
49
49
  const LINK_SENDER_BROKEN = 1;
50
50
 
51
51
  /** The device part of a channel address: `ABC0000001:1` -> `ABC0000001`. */
52
+ /** An SGTIN or an HmIP device key as the comparisons take it: no dashes or blanks, upper case. */
53
+ function normaliseHex(text) {
54
+ return String(text || '')
55
+ .replace(/[\s-]/g, '')
56
+ .toUpperCase();
57
+ }
58
+
52
59
  function deviceOf(address) {
53
60
  const colon = String(address).indexOf(':');
54
61
  return colon === -1 ? String(address) : String(address).slice(0, colon);
@@ -285,6 +292,12 @@ class HmSim {
285
292
  this.keyMismatchScript = {};
286
293
  /** the serial getKeyMismatchDevice answers: {<iface>: serial} */
287
294
  this.keyMismatch = {};
295
+ /** the whitelist of the open install mode (setInstallModeWithWhitelist): {<iface>: [{ADDRESS, KEY_MODE, KEY}]} */
296
+ this.installWhitelist = {};
297
+ /** HmIP devices in reach of the next install mode: {<iface>: Map<sgtin, {sgtin, key, devices, delay}>}, see scriptInclusion() */
298
+ this.inclusionScripts = {};
299
+ /** every inclusion request a scripted device made: {<iface>: [{sgtin, result, ts}]}, see getInclusions() */
300
+ this.inclusions = {};
288
301
  /** suppressed service messages (hmipserver): {<iface>: Map<channelAddress, Set<parameter>>} */
289
302
  this.suppressed = {};
290
303
  /** metadata per interface and address: {<iface>: {<address>: {<key>: value}}} */
@@ -571,6 +584,28 @@ class HmSim {
571
584
  return this.requireDevice(iface, address);
572
585
  }
573
586
 
587
+ /**
588
+ * The RPC `getParamsetDescription`: unlike {@link getParamsetDescription}, which the simulator
589
+ * calls for itself, an address the interface does not know is a fault (rfd `-2 Unknown instance`,
590
+ * hmipserver `-2 Invalid device`, measured on 3.89.11), and so is a paramset the channel does not
591
+ * list in `PARAMSETS` (`-3` on both). A paramset the channel has but the description set lacks
592
+ * still answers `''`, logged once by {@link resolveParamsetKey} - that is a gap in the fixture,
593
+ * not something an interface process does.
594
+ * @param {string} iface
595
+ * @param {string} address device or channel address
596
+ * @param {string} paramset MASTER, VALUES, LINK or SERVICE
597
+ */
598
+ describeParamset(iface, address, paramset) {
599
+ const device = this.requireDevice(iface, address);
600
+ if (typeof paramset !== 'string' || paramset === '') {
601
+ throw this.fault('invalidArguments', 'paramset type');
602
+ }
603
+ if (Array.isArray(device.PARAMSETS) && !device.PARAMSETS.includes(paramset)) {
604
+ throw this.fault('unknownParamsetDescription', `${address}/${paramset}`);
605
+ }
606
+ return this.getParamsetDescription(iface, device, paramset);
607
+ }
608
+
574
609
  getValue(iface, address, datapoint) {
575
610
  const {description, values} = this.storedParamset(iface, address, 'VALUES');
576
611
  if (!description[datapoint]) {
@@ -949,14 +984,31 @@ class HmSim {
949
984
  }
950
985
  }
951
986
 
987
+ /**
988
+ * The peers of a channel: the other end of every link it is on, the own channel once for a link
989
+ * with itself. For a device address rfd answers `-1 Failure` (measured, 3.89.11); hmipserver
990
+ * answers the channels' lists one after the other, a peer linked with two of the channels twice
991
+ * (measured, 3.89.11) - the simulator does the same on every interface but `rfd`.
992
+ * @param {string} iface
993
+ * @param {Array} params [address]
994
+ * @returns {string[]} channel addresses
995
+ */
952
996
  getLinkPeers(iface, params) {
953
- const addresses = new Set(this.channelsOf(iface, params[0]));
997
+ const [address] = params || [];
998
+ const device = this.requireDevice(iface, address);
999
+ if (iface === 'rfd' && !device.PARENT) {
1000
+ // rfd's -1 Failure; -1 Generic error under hmipserver's default table
1001
+ throw this.fault('notSupported', `getLinkPeers of a device address: ${address}`);
1002
+ }
1003
+
954
1004
  const peers = [];
955
- for (const link of this.links[iface] || []) {
956
- if (addresses.has(link.SENDER)) {
957
- peers.push(link.RECEIVER);
958
- } else if (addresses.has(link.RECEIVER)) {
959
- peers.push(link.SENDER);
1005
+ for (const channel of this.channelsOf(iface, address)) {
1006
+ for (const link of this.links[iface] || []) {
1007
+ if (link.SENDER === channel) {
1008
+ peers.push(link.RECEIVER);
1009
+ } else if (link.RECEIVER === channel) {
1010
+ peers.push(link.SENDER);
1011
+ }
960
1012
  }
961
1013
  }
962
1014
  return peers;
@@ -1151,13 +1203,55 @@ class HmSim {
1151
1203
  */
1152
1204
  setInstallMode(iface, params) {
1153
1205
  const [on, time = 60, mode = 1] = params || [];
1206
+ delete this.installWhitelist[iface];
1154
1207
  if (!on) {
1155
1208
  this.installMode[iface] = 0;
1156
1209
  return '';
1157
1210
  }
1158
1211
 
1159
- this.installMode[iface] = Date.now() + Number(time) * 1000;
1160
1212
  this.log.info(iface, 'install mode on for', time, 'seconds, mode', mode);
1213
+ return this.openInstallMode(iface, time);
1214
+ }
1215
+
1216
+ /**
1217
+ * `setInstallModeWithWhitelist(on, time, whitelist)` - hmipserver only. Opens the install mode
1218
+ * for the devices of the whitelist, `[{ADDRESS: <SGTIN>, KEY_MODE, KEY}]`: `KEY_MODE: 'LOCAL'`
1219
+ * with the device key as `KEY` (32 hex digits, as the QR code holds it), any other mode without
1220
+ * one (the key server's key). `on: false` closes the install mode. The simulator's model: a
1221
+ * device of scriptInclusion() that is not on the list is not admitted, one with a `LOCAL` key
1222
+ * that is not its own is declined (nothing joins, getInclusions() records it), any other joins.
1223
+ */
1224
+ setInstallModeWithWhitelist(iface, params) {
1225
+ if (iface !== 'hmip') {
1226
+ throw this.fault('unknownMethod', `${iface}/setInstallModeWithWhitelist`);
1227
+ }
1228
+ const [on, time = 60, whitelist = []] = params || [];
1229
+ if (!on) {
1230
+ delete this.installWhitelist[iface];
1231
+ this.installMode[iface] = 0;
1232
+ return '';
1233
+ }
1234
+ if (
1235
+ !Array.isArray(whitelist) ||
1236
+ !whitelist.every((entry) => entry && typeof entry === 'object' && typeof entry.ADDRESS === 'string')
1237
+ ) {
1238
+ throw this.fault(
1239
+ 'invalidArguments',
1240
+ `${iface}/setInstallModeWithWhitelist wants [{ADDRESS, KEY_MODE, KEY}]`,
1241
+ );
1242
+ }
1243
+ this.installWhitelist[iface] = whitelist.map((entry) => ({
1244
+ ADDRESS: normaliseHex(entry.ADDRESS),
1245
+ KEY_MODE: typeof entry.KEY_MODE === 'string' ? entry.KEY_MODE : '',
1246
+ KEY: typeof entry.KEY === 'string' ? entry.KEY : '',
1247
+ }));
1248
+ this.log.info(iface, 'install mode on for', time, 'seconds, whitelist of', whitelist.length);
1249
+ return this.openInstallMode(iface, time);
1250
+ }
1251
+
1252
+ /** The install mode opens: the scripted devices come into reach. */
1253
+ openInstallMode(iface, time) {
1254
+ this.installMode[iface] = Date.now() + Number(time) * 1000;
1161
1255
 
1162
1256
  const scripted = this.newDevicesScript[iface];
1163
1257
  if (scripted) {
@@ -1170,9 +1264,95 @@ class HmSim {
1170
1264
  this.timer(() => this.hearKeyMismatchDevice(iface, mismatch), mismatch.delay);
1171
1265
  }
1172
1266
 
1267
+ for (const script of (this.inclusionScripts[iface] || new Map()).values()) {
1268
+ this.timer(() => this.hearInclusion(iface, script), script.delay);
1269
+ }
1270
+
1173
1271
  return '';
1174
1272
  }
1175
1273
 
1274
+ /* ------------------------------------------------------------------ HmIP inclusion */
1275
+
1276
+ /**
1277
+ * An HmIP device in factory state comes into reach of the next install mode and asks to join
1278
+ * (Homematic Manager task 88). `key` is its device key as the QR code holds it (32 hex digits;
1279
+ * case and dashes do not matter). Opened with setInstallMode, it joins (the key server knows its
1280
+ * key); opened with setInstallModeWithWhitelist, it joins when it is on the list and its entry's
1281
+ * `LOCAL` key is `key` or the entry asks the key server, is declined with another `LOCAL` key,
1282
+ * and is not admitted when it is not on the list. Declined and not admitted, it stays in reach
1283
+ * for the next install mode; joined, `devices` go out with `newDevices` and the script is used
1284
+ * up. Each request is recorded (getInclusions()).
1285
+ *
1286
+ * @param {string} iface
1287
+ * @param {string} sgtin
1288
+ * @param {object} [script]
1289
+ * @param {string} [script.key=''] its device key
1290
+ * @param {Array} [script.devices=[]] its description and channel descriptions
1291
+ * @param {number} [script.delay=0] milliseconds between the install mode and the request
1292
+ */
1293
+ scriptInclusion(iface, sgtin, {key = '', devices = [], delay = 0} = {}) {
1294
+ if (typeof sgtin !== 'string' || sgtin === '') {
1295
+ throw new TypeError('scriptInclusion wants an SGTIN');
1296
+ }
1297
+ if (!this.inclusionScripts[iface]) {
1298
+ this.inclusionScripts[iface] = new Map();
1299
+ }
1300
+ const normalised = normaliseHex(sgtin);
1301
+ this.inclusionScripts[iface].set(normalised, {
1302
+ sgtin: normalised,
1303
+ key: normaliseHex(key),
1304
+ devices: devices || [],
1305
+ delay,
1306
+ });
1307
+ }
1308
+
1309
+ /** The scripted device asks to join: joined, declined for its key, or not admitted. */
1310
+ hearInclusion(iface, script) {
1311
+ const scripts = this.inclusionScripts[iface];
1312
+ if (!scripts || scripts.get(script.sgtin) !== script || this.getInstallMode(iface) === 0) {
1313
+ return;
1314
+ }
1315
+ let result = 'paired';
1316
+ const whitelist = this.installWhitelist[iface];
1317
+ if (whitelist) {
1318
+ const entry = whitelist.find((candidate) => candidate.ADDRESS === script.sgtin);
1319
+ if (!entry) {
1320
+ result = 'ignored';
1321
+ } else if (entry.KEY_MODE === 'LOCAL' && normaliseHex(entry.KEY) !== script.key) {
1322
+ result = 'declined';
1323
+ }
1324
+ }
1325
+ if (!this.inclusions[iface]) {
1326
+ this.inclusions[iface] = [];
1327
+ }
1328
+ this.inclusions[iface].push({sgtin: script.sgtin, result, ts: Date.now()});
1329
+ this.log.info(
1330
+ iface,
1331
+ script.sgtin,
1332
+ result === 'paired'
1333
+ ? 'joined'
1334
+ : result === 'declined'
1335
+ ? 'declined, the key does not match'
1336
+ : 'not on the whitelist',
1337
+ );
1338
+ if (result === 'paired') {
1339
+ scripts.delete(script.sgtin);
1340
+ if (script.devices.length > 0) {
1341
+ this.addDevices(iface, script.devices);
1342
+ }
1343
+ }
1344
+ }
1345
+
1346
+ /** Every inclusion request of a scripted device, oldest first: `[{sgtin, result, ts}]`, result `paired`, `declined` or `ignored`. */
1347
+ getInclusions(iface) {
1348
+ return [...(this.inclusions[iface] || [])];
1349
+ }
1350
+
1351
+ /** The whitelist of the open install mode, `[]` when it was opened without one or is closed. */
1352
+ getInstallWhitelist(iface) {
1353
+ return this.getInstallMode(iface) === 0 ? [] : structuredClone(this.installWhitelist[iface] || []);
1354
+ }
1355
+
1176
1356
  /* ------------------------------------------------------------------ key mismatch */
1177
1357
 
1178
1358
  /**
@@ -2416,7 +2596,7 @@ class HmSim {
2416
2596
  'system.multicall': (iface, params) => this.multicall(iface, params),
2417
2597
  init: (iface, params) => this.init(iface, params),
2418
2598
  listDevices: (iface) => this.devices[iface].devices,
2419
- getParamsetDescription: (iface, params) => this.getParamsetDescription(iface, params[0], params[1]),
2599
+ getParamsetDescription: (iface, params) => this.describeParamset(iface, params[0], params[1]),
2420
2600
  getDeviceDescription: (iface, params) => this.getDeviceDescription(iface, params[0]),
2421
2601
  ping: (iface, params) => this.ping(iface, params),
2422
2602
  setValue: (iface, params) => {
@@ -2443,6 +2623,7 @@ class HmSim {
2443
2623
  setBidcosInterface: (iface, params) => this.setBidcosInterface(iface, params),
2444
2624
  getServiceMessages: (iface) => this.getServiceMessages(iface),
2445
2625
  setInstallMode: (iface, params) => this.setInstallMode(iface, params),
2626
+ setInstallModeWithWhitelist: (iface, params) => this.setInstallModeWithWhitelist(iface, params),
2446
2627
  getInstallMode: (iface) => this.getInstallMode(iface),
2447
2628
  setTempKey: (iface, params) => this.setTempKey(iface, params),
2448
2629
  listTeams: (iface) => this.listTeams(iface),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hm-simulator",
3
- "version": "1.3.1",
3
+ "version": "1.4.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
@@ -238,6 +238,31 @@ declare namespace HmSim {
238
238
  args?: unknown[];
239
239
  }
240
240
 
241
+ /** scriptInclusion(): an HmIP device in reach of the next install mode */
242
+ interface InclusionScript {
243
+ /** its device key as the QR code holds it (32 hex digits) */
244
+ key?: string;
245
+ /** its description and channel descriptions, paired when it joins */
246
+ devices?: DeviceDescription[];
247
+ /** milliseconds between the install mode and its request */
248
+ delay?: number;
249
+ }
250
+
251
+ /** One inclusion request of a scripted HmIP device. */
252
+ interface Inclusion {
253
+ sgtin: string;
254
+ /** joined; declined because the whitelist's LOCAL key is not its key; not on the whitelist */
255
+ result: 'paired' | 'declined' | 'ignored';
256
+ ts: number;
257
+ }
258
+
259
+ /** One entry of setInstallModeWithWhitelist's list, as the simulator keeps it. */
260
+ interface WhitelistEntry {
261
+ ADDRESS: string;
262
+ KEY_MODE: string;
263
+ KEY: string;
264
+ }
265
+
241
266
  interface KeyMismatchScript {
242
267
  /** the passphrase the device was taught in with */
243
268
  key?: string;
@@ -349,6 +374,12 @@ declare class HmSim {
349
374
  keyMismatch: Record<string, string>;
350
375
  /** what scriptKeyMismatch() set up and did not pair yet */
351
376
  keyMismatchScript: Record<string, Required<HmSim.KeyMismatchScript> & {serial: string}>;
377
+ /** the whitelist the install mode was last opened with (setInstallModeWithWhitelist), per interface */
378
+ installWhitelist: Record<string, HmSim.WhitelistEntry[]>;
379
+ /** the HmIP devices scriptInclusion() put in reach and that did not join yet, per interface and SGTIN */
380
+ inclusionScripts: Record<string, Map<string, Required<HmSim.InclusionScript> & {sgtin: string}>>;
381
+ /** every inclusion request of a scripted device, per interface */
382
+ inclusions: Record<string, HmSim.Inclusion[]>;
352
383
  /** the temporary key per interface, as setTempKey left it */
353
384
  tempKey: Record<string, string>;
354
385
  /** the log level per interface, as logLevel set it */
@@ -381,6 +412,12 @@ declare class HmSim {
381
412
  scriptNewDevices(iface: string, devices: HmSim.DeviceDescription[], delay?: number): void;
382
413
  /** A device that holds another system's key: heard at the next install mode, paired with its key. */
383
414
  scriptKeyMismatch(iface: string, serial: string, script?: HmSim.KeyMismatchScript): void;
415
+ /** An HmIP device in reach of the next install mode: joins, or is declined for a whitelist key that is not its own. */
416
+ scriptInclusion(iface: string, sgtin: string, script?: HmSim.InclusionScript): void;
417
+ /** Every inclusion request of a scripted device, oldest first. */
418
+ getInclusions(iface: string): HmSim.Inclusion[];
419
+ /** The whitelist of the open install mode, `[]` without one. */
420
+ getInstallWhitelist(iface: string): HmSim.WhitelistEntry[];
384
421
 
385
422
  /* ---------------------------------------------------------------- scenario api: values and events */
386
423
 
@@ -405,6 +442,8 @@ declare class HmSim {
405
442
  paramset: string,
406
443
  ): HmSim.ParamsetDescription | undefined;
407
444
  getDeviceDescription(iface: string, address: string): HmSim.DeviceDescription;
445
+ /** the RPC `getParamsetDescription`: faults for an unknown address or a paramset the channel lacks */
446
+ describeParamset(iface: string, address: string, paramset: string): HmSim.ParamsetDescription | undefined;
408
447
 
409
448
  /* ---------------------------------------------------------------- scenario api: service messages and health */
410
449