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 +24 -0
- package/README.md +70 -28
- package/lib/control.js +3 -0
- package/lib/faults.d.ts +1 -0
- package/lib/faults.js +8 -1
- package/lib/method-help.js +2 -0
- package/lib/sim.js +189 -8
- package/package.json +1 -1
- package/sim.d.ts +39 -0
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.
|
|
500
|
-
|
|
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
|
|
539
|
-
|
|
|
540
|
-
| `unknownMethod`
|
|
541
|
-
| `unknownInstance`
|
|
542
|
-
| `unknownParamset`
|
|
543
|
-
| `
|
|
544
|
-
| `
|
|
545
|
-
| `
|
|
546
|
-
| `
|
|
547
|
-
| `
|
|
548
|
-
| `
|
|
549
|
-
| `
|
|
550
|
-
| `
|
|
551
|
-
| `
|
|
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`, `
|
|
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
package/lib/faults.d.ts
CHANGED
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
|
package/lib/method-help.js
CHANGED
|
@@ -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
|
|
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
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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.
|
|
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
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
|
|