hm-simulator 1.2.0 → 1.3.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 +18 -0
- package/README.md +85 -34
- package/lib/control.js +1 -0
- package/lib/faults.d.ts +48 -0
- package/lib/method-help.js +14 -0
- package/lib/sim.js +345 -7
- package/package.json +10 -4
- package/sim.d.mts +5 -0
- package/sim.d.ts +443 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
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.3.0
|
|
7
|
+
|
|
8
|
+
- The calls Homematic Manager makes, measured on rfd and hmipserver of firmware 3.89.11:
|
|
9
|
+
`getKeyMismatchDevice` with the scenario call `scriptKeyMismatch` (a device that holds another
|
|
10
|
+
system's key: heard at the next install mode, paired once `setTempKey` offers its key) and rfd's
|
|
11
|
+
`addDevice(serial)`; `suppressServiceMessages` and `getSuppressedServiceMessages` on hmipserver
|
|
12
|
+
(a suppressed message is left out of `getServiceMessages` and reports the value that raises
|
|
13
|
+
none); `getMetadata`, `setMetadata` and rfd's `getAllMetadata`, seeded with the new `metadata`
|
|
14
|
+
option; `listReplaceableDevices`; `getVersion` (`interfaces.<iface>.version`); `getLGWStatus`
|
|
15
|
+
(only with `interfaces.<iface>.lgwStatus` - rfd 3.89.11 does not know it); `setInterfaceClock`
|
|
16
|
+
(recorded in `sim.interfaceClocks`).
|
|
17
|
+
- TypeScript declarations: `sim.d.ts` (the `types` of the package), `sim.d.mts` for `sim.mjs` and
|
|
18
|
+
`lib/faults.d.ts`, covering the options, the scenario API, the introspection calls and the
|
|
19
|
+
description types. `npm test` type-checks them against a file that uses every export
|
|
20
|
+
(`test/types/`), and a test fails when an option or scenario call is missing from them.
|
|
21
|
+
- `interfaces.<iface>.getServiceMessagesFault: true` answers `getServiceMessages` with
|
|
22
|
+
`Invalid XML-RPC message`, as VirtualDevices, CUxD and hmipserver were seen to on 3.89.x.
|
|
23
|
+
|
|
6
24
|
## 1.2.0
|
|
7
25
|
|
|
8
26
|
- The README is rewritten for the test author (quick start, connecting a client, the scenario API
|
package/README.md
CHANGED
|
@@ -38,6 +38,10 @@ Prerequisites: [Node.js](https://nodejs.org) >= 20.19.
|
|
|
38
38
|
npm install --save-dev hm-simulator
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
TypeScript declarations ship with the package (`sim.d.ts`, `sim.d.mts`, `lib/faults.d.ts`): the
|
|
42
|
+
options, the scenario API, the introspection calls and the description types are under the `HmSim`
|
|
43
|
+
namespace (`HmSim.Options`, `HmSim.DeviceDescription`, `HmSim.ParamsetDescription`, ...).
|
|
44
|
+
|
|
41
45
|
A test file with `node:test` - the same shape works with mocha (`before`/`after`) and vitest
|
|
42
46
|
(`beforeAll`/`afterAll`):
|
|
43
47
|
|
|
@@ -118,15 +122,17 @@ that starts with the bytes `Bin` is BIN-RPC, anything else XML-RPC over HTTP.
|
|
|
118
122
|
|
|
119
123
|
**Incoming RPC methods.**
|
|
120
124
|
|
|
121
|
-
|
|
|
122
|
-
|
|
|
123
|
-
| session
|
|
124
|
-
| devices
|
|
125
|
-
| paramsets
|
|
126
|
-
| links
|
|
127
|
-
| interface
|
|
128
|
-
|
|
|
129
|
-
|
|
|
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) |
|
|
130
136
|
|
|
131
137
|
Every write is checked against the paramset description (type, range, `VALUE_LIST`,
|
|
132
138
|
`OPERATIONS`) and answered the way the interface process answers it - see
|
|
@@ -179,30 +185,31 @@ UTF-8. BIN-RPC strings go out as ISO-8859-1, as rfd sends them.
|
|
|
179
185
|
|
|
180
186
|
`new HmSim(options)`; everything is optional.
|
|
181
187
|
|
|
182
|
-
| option | default |
|
|
183
|
-
| -------------------------- | ---------------------------------------------------- |
|
|
184
|
-
| `devices` | `{rfd: {devices: []}, hmip: {devices: []}}` | device descriptions per interface, `{<iface>: {devices: [...]}}`
|
|
185
|
-
| `paramsetDescriptions` | the bundled 8.4 MB `data/paramset-descriptions.json` | replaces the bundled descriptions, see [Device data and fixtures](#device-data-and-fixtures)
|
|
186
|
-
| `paramsetFallback` | `true` | a firmware without a description uses the nearest one; `false` for exact keys only
|
|
187
|
-
| `links` | `{}` | links per interface, `[{SENDER, RECEIVER, FLAGS, NAME, DESCRIPTION}]`
|
|
188
|
-
| `bidcosInterfaces` | one per interface | what `listBidcosInterfaces` answers, per interface
|
|
189
|
-
| `serviceMessages` | `{}` | extra service messages per interface, `[[address, datapoint, value]]`
|
|
190
|
-
| `newDevices` | `{}` | devices that appear when the install mode is switched on, `{<iface>: {devices, delay}}`
|
|
191
|
-
| `defaultRssi` | `-65` | what `rssiInfo` answers where a device has no RSSI datapoint
|
|
192
|
-
| `
|
|
193
|
-
| `config.
|
|
194
|
-
| `config.
|
|
195
|
-
| `config.
|
|
196
|
-
| `config.
|
|
197
|
-
| `config.
|
|
198
|
-
| `config.
|
|
199
|
-
| `
|
|
200
|
-
| `
|
|
201
|
-
| `
|
|
202
|
-
| `
|
|
203
|
-
| `
|
|
204
|
-
| `
|
|
205
|
-
| `
|
|
188
|
+
| option | default | |
|
|
189
|
+
| -------------------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
190
|
+
| `devices` | `{rfd: {devices: []}, hmip: {devices: []}}` | device descriptions per interface, `{<iface>: {devices: [...]}}` |
|
|
191
|
+
| `paramsetDescriptions` | the bundled 8.4 MB `data/paramset-descriptions.json` | replaces the bundled descriptions, see [Device data and fixtures](#device-data-and-fixtures) |
|
|
192
|
+
| `paramsetFallback` | `true` | a firmware without a description uses the nearest one; `false` for exact keys only |
|
|
193
|
+
| `links` | `{}` | links per interface, `[{SENDER, RECEIVER, FLAGS, NAME, DESCRIPTION}]` |
|
|
194
|
+
| `bidcosInterfaces` | one per interface | what `listBidcosInterfaces` answers, per interface |
|
|
195
|
+
| `serviceMessages` | `{}` | extra service messages per interface, `[[address, datapoint, value]]` |
|
|
196
|
+
| `newDevices` | `{}` | devices that appear when the install mode is switched on, `{<iface>: {devices, delay}}` |
|
|
197
|
+
| `defaultRssi` | `-65` | what `rssiInfo` answers where a device has no RSSI datapoint |
|
|
198
|
+
| `metadata` | `{}` | what `getMetadata` answers before any `setMetadata`, `{<iface>: {<address>: {<key>: value}}}` |
|
|
199
|
+
| `config.listenAddress` | all interfaces | `'127.0.0.1'` in tests |
|
|
200
|
+
| `config.binrpcListenPort` | – | rfd |
|
|
201
|
+
| `config.xmlrpcListenPort` | – | hmipserver |
|
|
202
|
+
| `config.wiredListenPort` | – | BidCos-Wired, started when `devices.wired` exists |
|
|
203
|
+
| `config.virtualListenPort` | off | VirtualDevices |
|
|
204
|
+
| `config.virtualPath` | `/groups` | the path VirtualDevices answers on |
|
|
205
|
+
| `config.cuxdListenPort` | off | CUxD |
|
|
206
|
+
| `interfaces` | see below | behaviour per interface, `{<iface>: {...}}` |
|
|
207
|
+
| `faults` | hmipserver's table | overrides for the fault table, see [Fault codes](#fault-codes) |
|
|
208
|
+
| `behaviorPath` | `behaviors/` of the package | directory with [behaviour scripts](#behaviour-scripts), `false` for none |
|
|
209
|
+
| `rega` | off | the [ReGa mock](#rega-mock) |
|
|
210
|
+
| `tls` | off | `true` or `{key, cert}`, see [TLS and basic auth](#tls-and-basic-auth) |
|
|
211
|
+
| `auth` | off | `{username, password}`, HTTP basic auth |
|
|
212
|
+
| `log` | silent | an object with `debug`, `info`, `warn`, `error` (`console` will do) |
|
|
206
213
|
|
|
207
214
|
**Per interface** (`interfaces: {hmip: {...}, rfd: {...}}`):
|
|
208
215
|
|
|
@@ -220,6 +227,9 @@ UTF-8. BIN-RPC strings go out as ISO-8859-1, as rfd sends them.
|
|
|
220
227
|
| `startDelay` | `0` | milliseconds after `whenReady()` until the port accepts connections: a process that starts late (`sim.ports` knows the port from the start) |
|
|
221
228
|
| `unreachWrites` | `'accept'` | what `setValue`/`putParamset` to an unreachable device answer: `'accept'`, or `'fault'` (`notReachable`). The simulator's model, not a measurement |
|
|
222
229
|
| `firmwareUpdateDelay` | `0` | milliseconds per step of a firmware update |
|
|
230
|
+
| `getServiceMessagesFault` | `false` | answer `getServiceMessages` with the unknown-method fault (`Invalid XML-RPC message`), as VirtualDevices, CUxD and hmipserver were seen to on 3.89.x |
|
|
231
|
+
| `version` | `'2.6.0'` for `rfd` and `wired`, `'3.89.11.20260919'` for `hmip` | what `getVersion` answers; the rest has no `getVersion` |
|
|
232
|
+
| `lgwStatus` | – | BidCos only: what `getLGWStatus` answers; without it the method is unknown, as on rfd 3.89.11 |
|
|
223
233
|
|
|
224
234
|
## Scenario API
|
|
225
235
|
|
|
@@ -233,6 +243,9 @@ sim.addDevice('rfd', device, channel0, channel1); // pairs a device: newDevices
|
|
|
233
243
|
sim.removeDevice('rfd', 'LAB0000002'); // deleteDevices, with its values and links
|
|
234
244
|
sim.scriptNewDevices('rfd', [device, channel0, channel1], 500); // appear 500 ms after setInstallMode
|
|
235
245
|
sim.getDevice('rfd', 'LAB0000002:1'); // the description, false when unknown
|
|
246
|
+
// a device that holds another system's key: the next install mode hears it (getKeyMismatchDevice
|
|
247
|
+
// names it) until setTempKey('their-key') lets it pair; addDevice('LAB0000009') does the same
|
|
248
|
+
sim.scriptKeyMismatch('rfd', 'LAB0000009', {key: 'their-key', devices: [device, channel0, channel1]});
|
|
236
249
|
```
|
|
237
250
|
|
|
238
251
|
**Values and events**
|
|
@@ -285,6 +298,8 @@ sim.getTempKey('rfd'); // what setTempKey set
|
|
|
285
298
|
sim.getInstallMode('rfd'); // seconds of install mode left
|
|
286
299
|
sim.keyChanges; // every changeKey: [{iface, key, ts}]
|
|
287
300
|
sim.firmwareListRefreshes; // every refreshDeployedDeviceFirmwareList: [{iface, ts}]
|
|
301
|
+
sim.interfaceClocks; // every setInterfaceClock: [{iface, utc, offset, ts}]
|
|
302
|
+
sim.metadata; // what setMetadata stored: {<iface>: {<address>: {<key>: value}}}
|
|
288
303
|
sim.ports; // {rfd, hmip, wired, virtual, cuxd, ...}: the ports once whenReady() resolved
|
|
289
304
|
sim.regaSim.scripts; // every script the ReGa mock received
|
|
290
305
|
```
|
|
@@ -543,6 +558,20 @@ nothing is pending (measured); `serviceMessagesEmptyAsString: true` does the sam
|
|
|
543
558
|
because it breaks every client that assumes an array, which is why a client should be tested with it
|
|
544
559
|
once. Teams (`listTeams`) have the shape rfd was read to have on a CCU3.
|
|
545
560
|
|
|
561
|
+
**Suppression** (hmipserver only, eQ-3's HmIP addendum): `suppressServiceMessages(channelAddress,
|
|
562
|
+
parameter, suppress)` takes one of the channel's service parameters or `''` for all of them, and
|
|
563
|
+
ignores a parameter without the service flag; `getSuppressedServiceMessages(channelAddress)`
|
|
564
|
+
answers the suppressed ones (`[]` for an address it does not know). Measured on 3.89.11. A
|
|
565
|
+
suppressed message is left out of `getServiceMessages` and its datapoint reports the value that
|
|
566
|
+
raises no message (`false`), in `getValue`, `getParamset` and events, while the stored value stays;
|
|
567
|
+
a new occurrence stays suppressed until the suppression is lifted. That a change of the suppression
|
|
568
|
+
sends that value as an event is the simulator's model.
|
|
569
|
+
|
|
570
|
+
VirtualDevices, CUxD and hmipserver were seen to answer `getServiceMessages` with the fault
|
|
571
|
+
`Invalid XML-RPC message` on 3.89.x (hmipserver answered it on 3.89.11);
|
|
572
|
+
`interfaces.<iface>.getServiceMessagesFault: true` does the same, for a client that has to survive
|
|
573
|
+
it.
|
|
574
|
+
|
|
546
575
|
### A callback server that hangs
|
|
547
576
|
|
|
548
577
|
`interfaces.<iface>.listenerModel: 'measured'`. Measured on firmware 3.89.8 (2026-09-25) with a
|
|
@@ -563,6 +592,28 @@ measurement; so is everything with the default `'isolated'`, where every client
|
|
|
563
592
|
(rfd, hmipserver; recorded in `sim.firmwareListRefreshes`) answer as the interfaces were measured to
|
|
564
593
|
on firmware 3.89.x; the simulator does no cryptography and loads no firmware.
|
|
565
594
|
|
|
595
|
+
### The calls of Homematic Manager
|
|
596
|
+
|
|
597
|
+
Measured on rfd and hmipserver of firmware 3.89.11 (read-only calls, and one suppression set and
|
|
598
|
+
lifted again):
|
|
599
|
+
|
|
600
|
+
| call | rfd | hmipserver |
|
|
601
|
+
| ---------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------- |
|
|
602
|
+
| `getKeyMismatchDevice(reset)` | `''` when no device holds another key | the same |
|
|
603
|
+
| `getMetadata(address, key)`, key not set | fault `-1 Failure`, for an unknown address too | an empty value |
|
|
604
|
+
| `getAllMetadata(address)`, nothing set | fault `-1 Failure` | not in `system.listMethods` |
|
|
605
|
+
| `listReplaceableDevices(address)` | `[]` for its own `BidCoS-RF`, `-2 Unknown instance` for an unknown one | an empty HTTP body, always |
|
|
606
|
+
| `getVersion()` | `2.6.0` | the firmware, `3.89.11.…` |
|
|
607
|
+
| `getLGWStatus()` | `unknown method name`, with and without a LAN gateway in the system | not in `system.listMethods` |
|
|
608
|
+
| `suppressServiceMessages(…)` | not in `system.listMethods` | see [Service messages](#service-messages) |
|
|
609
|
+
|
|
610
|
+
The simulator answers so, with the fault table in force (`-1 Generic error` under hmipserver's
|
|
611
|
+
default table where rfd says `-1 Failure`) and `''` for hmipserver's empty body. Its models:
|
|
612
|
+
`listReplaceableDevices` on rfd lists the other devices of the same `TYPE`; `setMetadata` on rfd
|
|
613
|
+
knows only its own addresses; `setInterfaceClock` is recorded, not measured (it sets the clock the
|
|
614
|
+
devices get). `addDevice(serial)` pairs only the device of `scriptKeyMismatch`, with the right
|
|
615
|
+
temporary key; any other serial is `unknownInstance`, the mismatch a `notSupported` fault.
|
|
616
|
+
|
|
566
617
|
## ReGa mock
|
|
567
618
|
|
|
568
619
|
`rega: {port: 8181, ...}` starts an HTTP server that answers `POST /<name>.exe` as the CCU's
|
|
@@ -674,7 +725,7 @@ there. The flags win over the file:
|
|
|
674
725
|
arguments. The answer is `{"result": ...}`; a fault is `400` with `faultCode` and `faultString`, an
|
|
675
726
|
unknown call `404`. `GET /ports` answers the ports. The calls: `addDevice`, `removeDevice`,
|
|
676
727
|
`fireEvent`, `fireEvents`, `setValue` (the device reporting a value, as a behaviour script does),
|
|
677
|
-
`setServiceMessage`, `scriptNewDevices`, `dropConnection`, `stopInterface`, `startInterface`,
|
|
728
|
+
`setServiceMessage`, `scriptNewDevices`, `scriptKeyMismatch`, `dropConnection`, `stopInterface`, `startInterface`,
|
|
678
729
|
`restartInterface`, `injectFault`, `clearFaults`, `setReachable`, `setLowBattery`, `setDutyCycle`,
|
|
679
730
|
`setCarrierSense`, `offerFirmware`, `schedule`, `getDevice`, `getWriteLog`, `getConfigPending`,
|
|
680
731
|
`getPoisonedChannels`, `getMissingParamsetDescriptions`, `getCallbackLog`, `getTempKey`,
|
package/lib/control.js
CHANGED
package/lib/faults.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Type declarations for lib/faults.js (`require('hm-simulator/lib/faults.js')`).
|
|
2
|
+
|
|
3
|
+
/** An XML-RPC fault: what the interface process answers. */
|
|
4
|
+
export interface FaultEntry {
|
|
5
|
+
faultCode: number;
|
|
6
|
+
faultString: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** The names of the fault table's entries. */
|
|
10
|
+
export type FaultName =
|
|
11
|
+
| 'unknownMethod'
|
|
12
|
+
| 'unknownInstance'
|
|
13
|
+
| 'unknownParamset'
|
|
14
|
+
| 'unknownParameter'
|
|
15
|
+
| 'unknownLink'
|
|
16
|
+
| 'readOnly'
|
|
17
|
+
| 'typeError'
|
|
18
|
+
| 'outOfRange'
|
|
19
|
+
| 'invalidValue'
|
|
20
|
+
| 'notSupported'
|
|
21
|
+
| 'notReachable'
|
|
22
|
+
| 'invalidArguments';
|
|
23
|
+
|
|
24
|
+
/** A fault table: every name above, plus whatever an override added. */
|
|
25
|
+
export type FaultTable = Record<FaultName, FaultEntry> & Record<string, FaultEntry>;
|
|
26
|
+
|
|
27
|
+
/** hmipserver 3.89.8, measured 2026-09-05 - the default. */
|
|
28
|
+
export declare const DEFAULT_FAULTS: FaultTable;
|
|
29
|
+
/** rfd 3.89.8, measured 2026-09-05. */
|
|
30
|
+
export declare const BIDCOS_FAULTS: FaultTable;
|
|
31
|
+
/** The measured tables by name, for `new HmSim({faults: FAULT_TABLES.bidcos})`. */
|
|
32
|
+
export declare const FAULT_TABLES: {hmip: FaultTable; bidcos: FaultTable};
|
|
33
|
+
|
|
34
|
+
/** Error carrying an XML-RPC fault. */
|
|
35
|
+
export declare class RpcFault extends Error {
|
|
36
|
+
constructor(faultCode: number, faultString: string, detail?: string);
|
|
37
|
+
faultCode: number;
|
|
38
|
+
faultString: string;
|
|
39
|
+
detail?: string;
|
|
40
|
+
/** The struct a BIN-RPC fault carries as its body. */
|
|
41
|
+
toStruct(): FaultEntry;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A fault factory bound to a (possibly overridden) table. */
|
|
45
|
+
export declare function createFaults(overrides?: Partial<Record<FaultName | (string & {}), Partial<FaultEntry>>>): {
|
|
46
|
+
table: FaultTable;
|
|
47
|
+
fault: (name: FaultName | (string & {}), detail?: string) => RpcFault;
|
|
48
|
+
};
|
package/lib/method-help.js
CHANGED
|
@@ -55,4 +55,18 @@ module.exports = {
|
|
|
55
55
|
restoreConfigToDevice: 'void restoreConfigToDevice(string address) - write the cached configuration to the device',
|
|
56
56
|
determineParameter:
|
|
57
57
|
'void determineParameter(string address, string paramsetKey, string parameterId) - read a parameter from the device',
|
|
58
|
+
getKeyMismatchDevice:
|
|
59
|
+
'string getKeyMismatchDevice(boolean reset) - the serial of a device that holds another key, reset clears it',
|
|
60
|
+
addDevice: 'struct addDevice(string serialNumber) - pair one device by its serial; BidCos only',
|
|
61
|
+
suppressServiceMessages:
|
|
62
|
+
'void suppressServiceMessages(string channelAddress, string parameter, boolean suppress) - suppress a service message, an empty parameter for all; HmIP only',
|
|
63
|
+
getSuppressedServiceMessages:
|
|
64
|
+
'array getSuppressedServiceMessages(string channelAddress) - the suppressed service parameters of a channel; HmIP only',
|
|
65
|
+
getMetadata: 'ValueType getMetadata(string address, string key) - a stored metadata value',
|
|
66
|
+
setMetadata: 'void setMetadata(string address, string key, ValueType value) - store a metadata value',
|
|
67
|
+
getAllMetadata: 'struct getAllMetadata(string address) - every metadata value of an address; BidCos only',
|
|
68
|
+
listReplaceableDevices: 'array listReplaceableDevices(string address) - the devices a device can replace',
|
|
69
|
+
getVersion: 'string getVersion() - the version of the interface process',
|
|
70
|
+
getLGWStatus: 'struct getLGWStatus() - the state of the LAN gateways',
|
|
71
|
+
setInterfaceClock: 'void setInterfaceClock(int utcSeconds, int offsetMinutes) - set the clock sent to the devices',
|
|
58
72
|
};
|
package/lib/sim.js
CHANGED
|
@@ -113,6 +113,9 @@ const INTERFACE_DEFAULTS = {
|
|
|
113
113
|
// rfd answers the empty string instead of an empty array when it has no service message; the
|
|
114
114
|
// real shape, off by default because it breaks every consumer that assumes an array
|
|
115
115
|
serviceMessagesEmptyAsString: false,
|
|
116
|
+
// answer getServiceMessages with the unknown-method fault, as VirtualDevices, CUxD and
|
|
117
|
+
// hmipserver were seen to on 3.89.x ("Invalid XML-RPC message"); off by default
|
|
118
|
+
getServiceMessagesFault: false,
|
|
116
119
|
};
|
|
117
120
|
|
|
118
121
|
/**
|
|
@@ -121,11 +124,15 @@ const INTERFACE_DEFAULTS = {
|
|
|
121
124
|
*/
|
|
122
125
|
const MEASURED_INTERFACE_DEFAULTS = {
|
|
123
126
|
// hmipserver answers ping without a PONG event, see https://github.com/eq-3/occu/issues/42
|
|
124
|
-
hmip: {configPendingMode: 'hmip', pong: false},
|
|
125
|
-
rfd: {configPendingMode: 'bidcos'},
|
|
126
|
-
|
|
127
|
+
hmip: {configPendingMode: 'hmip', pong: false, version: '3.89.11.20260919'},
|
|
128
|
+
rfd: {configPendingMode: 'bidcos', version: '2.6.0'},
|
|
129
|
+
// hs485d's getVersion was not measured; rfd's answer stands in
|
|
130
|
+
wired: {configPendingMode: 'bidcos', version: '2.6.0'},
|
|
127
131
|
};
|
|
128
132
|
|
|
133
|
+
/** the interfaces with the metadata methods: rfd and hs485d fault on a key that is not set, hmipserver answers '' */
|
|
134
|
+
const METADATA_INTERFACES = new Set(['rfd', 'wired', 'hmip']);
|
|
135
|
+
|
|
129
136
|
/** what listBidcosInterfaces answers when the `bidcosInterfaces` option does not say otherwise */
|
|
130
137
|
const DEFAULT_BIDCOS_INTERFACES = {
|
|
131
138
|
rfd: [
|
|
@@ -265,6 +272,16 @@ class HmSim {
|
|
|
265
272
|
this.unreachable = {};
|
|
266
273
|
/** what listBidcosInterfaces reports on top of the configured answer: {<iface>: {DUTY_CYCLE, ...}} */
|
|
267
274
|
this.radio = {};
|
|
275
|
+
/** a device that holds another system's key: {<iface>: {serial, key, devices, delay}}, see scriptKeyMismatch() */
|
|
276
|
+
this.keyMismatchScript = {};
|
|
277
|
+
/** the serial getKeyMismatchDevice answers: {<iface>: serial} */
|
|
278
|
+
this.keyMismatch = {};
|
|
279
|
+
/** suppressed service messages (hmipserver): {<iface>: Map<channelAddress, Set<parameter>>} */
|
|
280
|
+
this.suppressed = {};
|
|
281
|
+
/** metadata per interface and address: {<iface>: {<address>: {<key>: value}}} */
|
|
282
|
+
this.metadata = structuredClone(options.metadata || {});
|
|
283
|
+
/** every setInterfaceClock call: [{iface, utc, offset, ts}] */
|
|
284
|
+
this.interfaceClocks = [];
|
|
268
285
|
|
|
269
286
|
this.rpcMethods = this.buildRpcMethods();
|
|
270
287
|
|
|
@@ -550,7 +567,8 @@ class HmSim {
|
|
|
550
567
|
if (!description[datapoint]) {
|
|
551
568
|
throw this.fault('unknownParameter', `${address}/${datapoint}`);
|
|
552
569
|
}
|
|
553
|
-
|
|
570
|
+
const value = values[datapoint] === undefined ? defaultValue(description[datapoint]) : values[datapoint];
|
|
571
|
+
return this.reportedValue(iface, address, datapoint, value);
|
|
554
572
|
}
|
|
555
573
|
|
|
556
574
|
/**
|
|
@@ -563,7 +581,14 @@ class HmSim {
|
|
|
563
581
|
throw this.fault('invalidArguments', 'paramset key');
|
|
564
582
|
}
|
|
565
583
|
const {values} = this.storedParamset(iface, address, key);
|
|
566
|
-
|
|
584
|
+
if (key !== 'VALUES' || !this.suppressed[iface] || !this.suppressed[iface].has(address)) {
|
|
585
|
+
return {...values};
|
|
586
|
+
}
|
|
587
|
+
const reported = {};
|
|
588
|
+
for (const [name, value] of Object.entries(values)) {
|
|
589
|
+
reported[name] = this.reportedValue(iface, address, name, value);
|
|
590
|
+
}
|
|
591
|
+
return reported;
|
|
567
592
|
}
|
|
568
593
|
|
|
569
594
|
/**
|
|
@@ -1065,6 +1090,9 @@ class HmSim {
|
|
|
1065
1090
|
* plus what was raised through the scenario API.
|
|
1066
1091
|
*/
|
|
1067
1092
|
getServiceMessages(iface) {
|
|
1093
|
+
if (this.interfaceOption(iface, 'getServiceMessagesFault')) {
|
|
1094
|
+
throw this.fault('unknownMethod', `${iface}/getServiceMessages`);
|
|
1095
|
+
}
|
|
1068
1096
|
const messages = [];
|
|
1069
1097
|
for (const address of Object.keys(this.values[iface])) {
|
|
1070
1098
|
const description = this.getParamsetDescription(iface, address, 'VALUES');
|
|
@@ -1073,13 +1101,16 @@ class HmSim {
|
|
|
1073
1101
|
}
|
|
1074
1102
|
for (const [name, value] of Object.entries(this.values[iface][address].VALUES || {})) {
|
|
1075
1103
|
const parameter = description[name];
|
|
1076
|
-
if (parameter && parameter.FLAGS & SERVICE_FLAG && value) {
|
|
1104
|
+
if (parameter && parameter.FLAGS & SERVICE_FLAG && value && !this.isSuppressed(iface, address, name)) {
|
|
1077
1105
|
messages.push([address, name, value]);
|
|
1078
1106
|
}
|
|
1079
1107
|
}
|
|
1080
1108
|
}
|
|
1081
1109
|
|
|
1082
1110
|
for (const message of this.serviceMessages[iface] || []) {
|
|
1111
|
+
if (this.isSuppressed(iface, message[0], message[1])) {
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1083
1114
|
if (!messages.some((entry) => entry[0] === message[0] && entry[1] === message[1])) {
|
|
1084
1115
|
messages.push([...message]);
|
|
1085
1116
|
}
|
|
@@ -1115,6 +1146,292 @@ class HmSim {
|
|
|
1115
1146
|
this.timer(() => this.addDevices(iface, scripted.devices), scripted.delay);
|
|
1116
1147
|
}
|
|
1117
1148
|
|
|
1149
|
+
const mismatch = this.keyMismatchScript[iface];
|
|
1150
|
+
if (mismatch) {
|
|
1151
|
+
this.timer(() => this.hearKeyMismatchDevice(iface, mismatch), mismatch.delay);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
return '';
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/* ------------------------------------------------------------------ key mismatch */
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* A device that holds another system's security key comes into reach of the next install mode
|
|
1161
|
+
* (or of an `addDevice` with its serial) - the case of Homematic Manager task 76.
|
|
1162
|
+
*
|
|
1163
|
+
* rfd hears the device but cannot pair it: nothing joins and nobody is told; the serial is only
|
|
1164
|
+
* what `getKeyMismatchDevice` answers. Once the temporary key (`setTempKey`) is the device's
|
|
1165
|
+
* `key`, the next install mode pairs it: `devices` (the description and its channels) join with
|
|
1166
|
+
* `newDevices`, and the script is used up. Without `devices` the device is never paired.
|
|
1167
|
+
*
|
|
1168
|
+
* @param {string} iface
|
|
1169
|
+
* @param {string} serial
|
|
1170
|
+
* @param {object} [script]
|
|
1171
|
+
* @param {string} [script.key=''] the passphrase the device was taught in with
|
|
1172
|
+
* @param {Array} [script.devices=[]] its description and channel descriptions
|
|
1173
|
+
* @param {number} [script.delay=0] milliseconds between the install mode and the device being heard
|
|
1174
|
+
*/
|
|
1175
|
+
scriptKeyMismatch(iface, serial, {key = '', devices = [], delay = 0} = {}) {
|
|
1176
|
+
if (typeof serial !== 'string' || serial === '') {
|
|
1177
|
+
throw new TypeError('scriptKeyMismatch wants a serial');
|
|
1178
|
+
}
|
|
1179
|
+
this.keyMismatchScript[iface] = {serial, key: String(key), devices: devices || [], delay};
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/** The scripted device is heard: paired when the temporary key is its own, else a key mismatch. */
|
|
1183
|
+
hearKeyMismatchDevice(iface, script) {
|
|
1184
|
+
if (this.keyMismatchScript[iface] !== script) {
|
|
1185
|
+
return undefined;
|
|
1186
|
+
}
|
|
1187
|
+
if (script.key !== '' && this.getTempKey(iface) === script.key && script.devices.length > 0) {
|
|
1188
|
+
delete this.keyMismatchScript[iface];
|
|
1189
|
+
this.addDevices(iface, script.devices);
|
|
1190
|
+
this.log.info(iface, script.serial, 'paired with the temporary key');
|
|
1191
|
+
return this.index[iface].get(script.devices[0].ADDRESS) || script.devices[0];
|
|
1192
|
+
}
|
|
1193
|
+
this.keyMismatch[iface] = script.serial;
|
|
1194
|
+
this.log.info(iface, script.serial, 'heard, but it holds another key');
|
|
1195
|
+
return undefined;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* `getKeyMismatchDevice(reset)` - the serial of the last device that could not be paired
|
|
1200
|
+
* because it holds another key, `''` when there is none (measured on rfd and hmipserver,
|
|
1201
|
+
* firmware 3.89.11). `true` clears it after the answer.
|
|
1202
|
+
*/
|
|
1203
|
+
getKeyMismatchDevice(iface, params) {
|
|
1204
|
+
if (iface !== 'rfd' && iface !== 'hmip') {
|
|
1205
|
+
throw this.fault('unknownMethod', `${iface}/getKeyMismatchDevice`);
|
|
1206
|
+
}
|
|
1207
|
+
const [reset = false] = params || [];
|
|
1208
|
+
const serial = this.keyMismatch[iface] || '';
|
|
1209
|
+
if (reset) {
|
|
1210
|
+
delete this.keyMismatch[iface];
|
|
1211
|
+
}
|
|
1212
|
+
return serial;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* `addDevice(serial)` - rfd pairs one device by its serial. The simulator knows only the device
|
|
1217
|
+
* of scriptKeyMismatch(): with the right temporary key it joins and its description is the
|
|
1218
|
+
* answer; otherwise it is the key mismatch and the call faults. Any other serial is not in reach
|
|
1219
|
+
* (`unknownInstance`). BidCos only.
|
|
1220
|
+
*/
|
|
1221
|
+
addDeviceBySerial(iface, params) {
|
|
1222
|
+
if (!BIDCOS_INTERFACES.has(iface)) {
|
|
1223
|
+
throw this.fault('unknownMethod', `${iface}/addDevice`);
|
|
1224
|
+
}
|
|
1225
|
+
const [serial] = params || [];
|
|
1226
|
+
const script = this.keyMismatchScript[iface];
|
|
1227
|
+
if (!script || script.serial !== serial) {
|
|
1228
|
+
throw this.fault('unknownInstance', `${iface}/${serial}`);
|
|
1229
|
+
}
|
|
1230
|
+
const paired = this.hearKeyMismatchDevice(iface, script);
|
|
1231
|
+
if (!paired) {
|
|
1232
|
+
throw this.fault('notSupported', `${serial} holds another key`);
|
|
1233
|
+
}
|
|
1234
|
+
return paired;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/* ------------------------------------------------------------------ service message suppression */
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* `suppressServiceMessages(channelAddress, parameter, suppress)` - hmipserver only (eQ-3's HmIP
|
|
1241
|
+
* addendum; rfd does not have it). `parameter` is one of the channel's service parameters, or
|
|
1242
|
+
* `''` for all of them; a parameter without the service flag is ignored. Measured on firmware
|
|
1243
|
+
* 3.89.11: answered with an empty value, `''` suppressed every service parameter of the
|
|
1244
|
+
* channel, `LEVEL` nothing. A suppressed message is left out of `getServiceMessages`, and the
|
|
1245
|
+
* datapoint reports the value that raises no message (`false`, `0`) - the addendum's words; that
|
|
1246
|
+
* a change of the suppression sends that value as an event is the simulator's model.
|
|
1247
|
+
*/
|
|
1248
|
+
suppressServiceMessages(iface, params) {
|
|
1249
|
+
if (iface !== 'hmip') {
|
|
1250
|
+
throw this.fault('unknownMethod', `${iface}/suppressServiceMessages`);
|
|
1251
|
+
}
|
|
1252
|
+
const [address, parameter = '', suppress = true] = params || [];
|
|
1253
|
+
if (typeof address !== 'string' || typeof parameter !== 'string') {
|
|
1254
|
+
throw this.fault('invalidArguments', 'suppressServiceMessages wants a channel address and a parameter');
|
|
1255
|
+
}
|
|
1256
|
+
const description = this.getParamsetDescription(iface, address, 'VALUES') || {};
|
|
1257
|
+
const names = Object.keys(description).filter(
|
|
1258
|
+
(name) => description[name].FLAGS & SERVICE_FLAG && (parameter === '' || parameter === name),
|
|
1259
|
+
);
|
|
1260
|
+
const table = (this.suppressed[iface] = this.suppressed[iface] || new Map());
|
|
1261
|
+
const set = table.get(address) || new Set();
|
|
1262
|
+
for (const name of names) {
|
|
1263
|
+
const before = this.reportedValue(iface, address, name);
|
|
1264
|
+
if (suppress) {
|
|
1265
|
+
set.add(name);
|
|
1266
|
+
} else {
|
|
1267
|
+
set.delete(name);
|
|
1268
|
+
}
|
|
1269
|
+
table.set(address, set);
|
|
1270
|
+
const after = this.reportedValue(iface, address, name);
|
|
1271
|
+
if (before !== after && description[name].OPERATIONS & 4) {
|
|
1272
|
+
this.event(iface, [address, name, after]);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (set.size === 0) {
|
|
1276
|
+
table.delete(address);
|
|
1277
|
+
}
|
|
1278
|
+
return '';
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/**
|
|
1282
|
+
* `getSuppressedServiceMessages(channelAddress)` - the suppressed parameters of a channel, sorted;
|
|
1283
|
+
* `[]` for an address hmipserver does not know (measured, firmware 3.89.11). hmipserver only.
|
|
1284
|
+
*/
|
|
1285
|
+
getSuppressedServiceMessages(iface, params) {
|
|
1286
|
+
if (iface !== 'hmip') {
|
|
1287
|
+
throw this.fault('unknownMethod', `${iface}/getSuppressedServiceMessages`);
|
|
1288
|
+
}
|
|
1289
|
+
const [address] = params || [];
|
|
1290
|
+
const set = this.suppressed[iface] && this.suppressed[iface].get(address);
|
|
1291
|
+
return set ? [...set].sort() : [];
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
isSuppressed(iface, address, name) {
|
|
1295
|
+
const set = this.suppressed[iface] && this.suppressed[iface].get(address);
|
|
1296
|
+
return Boolean(set && set.has(name));
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/** The stored value, or for a suppressed service message the value that raises no message. */
|
|
1300
|
+
reportedValue(iface, address, name, value) {
|
|
1301
|
+
const stored =
|
|
1302
|
+
value === undefined
|
|
1303
|
+
? ((this.values[iface] && this.values[iface][address]) || {VALUES: {}}).VALUES[name]
|
|
1304
|
+
: value;
|
|
1305
|
+
if (!this.isSuppressed(iface, address, name)) {
|
|
1306
|
+
return stored;
|
|
1307
|
+
}
|
|
1308
|
+
return typeof stored === 'boolean' || stored === undefined ? false : 0;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/* ------------------------------------------------------------------ metadata */
|
|
1312
|
+
|
|
1313
|
+
requireMetadataInterface(iface, method) {
|
|
1314
|
+
if (!METADATA_INTERFACES.has(iface)) {
|
|
1315
|
+
throw this.fault('unknownMethod', `${iface}/${method}`);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* `getMetadata(address, key)` - a value stored with `setMetadata` or the `metadata` option. A key
|
|
1321
|
+
* that is not set: hmipserver answers an empty value, rfd the fault `-1 Failure` - also for an
|
|
1322
|
+
* address it does not know (measured, firmware 3.89.11). rfd, hs485d and hmipserver.
|
|
1323
|
+
*/
|
|
1324
|
+
getMetadata(iface, params) {
|
|
1325
|
+
this.requireMetadataInterface(iface, 'getMetadata');
|
|
1326
|
+
const [address, key] = params || [];
|
|
1327
|
+
const stored = this.metadata[iface] && this.metadata[iface][address];
|
|
1328
|
+
if (stored && Object.hasOwn(stored, key)) {
|
|
1329
|
+
return stored[key];
|
|
1330
|
+
}
|
|
1331
|
+
if (iface === 'hmip') {
|
|
1332
|
+
return '';
|
|
1333
|
+
}
|
|
1334
|
+
throw this.fault('notSupported', `no metadata ${key} on ${address}`);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* `setMetadata(address, key, value)` - stores a value. rfd and hs485d know only their own
|
|
1339
|
+
* addresses (`unknownInstance`); hmipserver takes any (the simulator's model).
|
|
1340
|
+
*/
|
|
1341
|
+
setMetadata(iface, params) {
|
|
1342
|
+
this.requireMetadataInterface(iface, 'setMetadata');
|
|
1343
|
+
const [address, key, value] = params || [];
|
|
1344
|
+
if (typeof address !== 'string' || typeof key !== 'string' || key === '') {
|
|
1345
|
+
throw this.fault('invalidArguments', 'setMetadata wants an address and a key');
|
|
1346
|
+
}
|
|
1347
|
+
if (iface !== 'hmip') {
|
|
1348
|
+
this.requireDevice(iface, address);
|
|
1349
|
+
}
|
|
1350
|
+
this.metadata[iface] = this.metadata[iface] || {};
|
|
1351
|
+
this.metadata[iface][address] = this.metadata[iface][address] || {};
|
|
1352
|
+
this.metadata[iface][address][key] = value;
|
|
1353
|
+
return '';
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* `getAllMetadata(address)` - every key of an address. rfd and hs485d only (hmipserver does not
|
|
1358
|
+
* list it); nothing stored is the fault `-1 Failure`, as measured on rfd.
|
|
1359
|
+
*/
|
|
1360
|
+
getAllMetadata(iface, params) {
|
|
1361
|
+
if (!BIDCOS_INTERFACES.has(iface)) {
|
|
1362
|
+
throw this.fault('unknownMethod', `${iface}/getAllMetadata`);
|
|
1363
|
+
}
|
|
1364
|
+
const [address] = params || [];
|
|
1365
|
+
const stored = this.metadata[iface] && this.metadata[iface][address];
|
|
1366
|
+
if (!stored || Object.keys(stored).length === 0) {
|
|
1367
|
+
throw this.fault('notSupported', `no metadata on ${address}`);
|
|
1368
|
+
}
|
|
1369
|
+
return {...stored};
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/* ------------------------------------------------------------------ interface information */
|
|
1373
|
+
|
|
1374
|
+
/**
|
|
1375
|
+
* `listReplaceableDevices(address)` - the devices a device can take the place of. rfd: the other
|
|
1376
|
+
* devices of the same `TYPE` (the simulator's model; measured: `[]` for rfd's own `BidCoS-RF`,
|
|
1377
|
+
* `-2 Unknown instance` for an address it does not know). hmipserver answers with an empty HTTP
|
|
1378
|
+
* body whatever it is asked (measured, firmware 3.89.11), which the simulator answers as `''`.
|
|
1379
|
+
*/
|
|
1380
|
+
listReplaceableDevices(iface, params) {
|
|
1381
|
+
const [address] = params || [];
|
|
1382
|
+
if (iface === 'hmip') {
|
|
1383
|
+
return '';
|
|
1384
|
+
}
|
|
1385
|
+
if (!BIDCOS_INTERFACES.has(iface)) {
|
|
1386
|
+
throw this.fault('unknownMethod', `${iface}/listReplaceableDevices`);
|
|
1387
|
+
}
|
|
1388
|
+
const device = this.requireDevice(iface, address);
|
|
1389
|
+
if (device.PARENT) {
|
|
1390
|
+
return [];
|
|
1391
|
+
}
|
|
1392
|
+
return this.deviceList(iface).filter((entry) => entry.TYPE === device.TYPE && entry.ADDRESS !== address);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/**
|
|
1396
|
+
* `getVersion()` - the version string of the interface process: `interfaces.<iface>.version`,
|
|
1397
|
+
* by default what rfd (`2.6.0`) and hmipserver (the firmware, `3.89.11.20260919`) answered on
|
|
1398
|
+
* firmware 3.89.11. VirtualDevices and CUxD: unknown-method.
|
|
1399
|
+
*/
|
|
1400
|
+
getVersion(iface) {
|
|
1401
|
+
const version = this.interfaceOption(iface, 'version');
|
|
1402
|
+
if (version === undefined) {
|
|
1403
|
+
throw this.fault('unknownMethod', `${iface}/getVersion`);
|
|
1404
|
+
}
|
|
1405
|
+
return version;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/**
|
|
1409
|
+
* `getLGWStatus()` - rfd on firmware 3.89.11 answers `unknown method name`, measured on two
|
|
1410
|
+
* systems; `interfaces.rfd.lgwStatus` makes it answer that value instead, for a client that
|
|
1411
|
+
* shows it. BidCos only.
|
|
1412
|
+
*/
|
|
1413
|
+
getLGWStatus(iface) {
|
|
1414
|
+
const status = BIDCOS_INTERFACES.has(iface) ? this.interfaceOption(iface, 'lgwStatus') : undefined;
|
|
1415
|
+
if (status === undefined) {
|
|
1416
|
+
throw this.fault('unknownMethod', `${iface}/getLGWStatus`);
|
|
1417
|
+
}
|
|
1418
|
+
return status;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* `setInterfaceClock(utcSeconds, offsetMinutes)` - rfd and hmipserver set the clock they send the
|
|
1423
|
+
* devices. Recorded in `sim.interfaceClocks` (`{iface, utc, offset, ts}`) and answered with an
|
|
1424
|
+
* empty string (the simulator's model; not measured - it changes the system's clock).
|
|
1425
|
+
*/
|
|
1426
|
+
setInterfaceClock(iface, params) {
|
|
1427
|
+
if (iface !== 'rfd' && iface !== 'hmip') {
|
|
1428
|
+
throw this.fault('unknownMethod', `${iface}/setInterfaceClock`);
|
|
1429
|
+
}
|
|
1430
|
+
const [utc, offset = 0] = params || [];
|
|
1431
|
+
if (!Number.isFinite(Number(utc))) {
|
|
1432
|
+
throw this.fault('invalidArguments', 'setInterfaceClock wants the UTC seconds');
|
|
1433
|
+
}
|
|
1434
|
+
this.interfaceClocks.push({iface, utc: Number(utc), offset: Number(offset), ts: Date.now()});
|
|
1118
1435
|
return '';
|
|
1119
1436
|
}
|
|
1120
1437
|
|
|
@@ -1365,6 +1682,12 @@ class HmSim {
|
|
|
1365
1682
|
if (this.poisonedChannels[iface]) {
|
|
1366
1683
|
this.poisonedChannels[iface].delete(entry);
|
|
1367
1684
|
}
|
|
1685
|
+
if (this.suppressed[iface]) {
|
|
1686
|
+
this.suppressed[iface].delete(entry);
|
|
1687
|
+
}
|
|
1688
|
+
if (this.metadata[iface]) {
|
|
1689
|
+
delete this.metadata[iface][entry];
|
|
1690
|
+
}
|
|
1368
1691
|
}
|
|
1369
1692
|
if (this.configPending[iface]) {
|
|
1370
1693
|
this.configPending[iface].delete(address);
|
|
@@ -2111,6 +2434,17 @@ class HmSim {
|
|
|
2111
2434
|
logLevel: (iface, params) => this.logLevel(iface, params),
|
|
2112
2435
|
changeKey: (iface, params) => this.changeKey(iface, params),
|
|
2113
2436
|
refreshDeployedDeviceFirmwareList: (iface) => this.refreshDeployedDeviceFirmwareList(iface),
|
|
2437
|
+
getKeyMismatchDevice: (iface, params) => this.getKeyMismatchDevice(iface, params),
|
|
2438
|
+
addDevice: (iface, params) => this.addDeviceBySerial(iface, params),
|
|
2439
|
+
suppressServiceMessages: (iface, params) => this.suppressServiceMessages(iface, params),
|
|
2440
|
+
getSuppressedServiceMessages: (iface, params) => this.getSuppressedServiceMessages(iface, params),
|
|
2441
|
+
getMetadata: (iface, params) => this.getMetadata(iface, params),
|
|
2442
|
+
setMetadata: (iface, params) => this.setMetadata(iface, params),
|
|
2443
|
+
getAllMetadata: (iface, params) => this.getAllMetadata(iface, params),
|
|
2444
|
+
listReplaceableDevices: (iface, params) => this.listReplaceableDevices(iface, params),
|
|
2445
|
+
getVersion: (iface) => this.getVersion(iface),
|
|
2446
|
+
getLGWStatus: (iface) => this.getLGWStatus(iface),
|
|
2447
|
+
setInterfaceClock: (iface, params) => this.setInterfaceClock(iface, params),
|
|
2114
2448
|
};
|
|
2115
2449
|
|
|
2116
2450
|
// the CCU accepts the lower case spelling as well
|
|
@@ -2580,7 +2914,11 @@ class HmSim {
|
|
|
2580
2914
|
const events =
|
|
2581
2915
|
parameter.TYPE === 'ACTION'
|
|
2582
2916
|
? [[address, datapoint, casted]]
|
|
2583
|
-
: Object.keys(state.VALUES).map((name) => [
|
|
2917
|
+
: Object.keys(state.VALUES).map((name) => [
|
|
2918
|
+
address,
|
|
2919
|
+
name,
|
|
2920
|
+
this.reportedValue(iface, address, name, state.VALUES[name]),
|
|
2921
|
+
]);
|
|
2584
2922
|
this.eventMulticall(iface, events);
|
|
2585
2923
|
}
|
|
2586
2924
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hm-simulator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Simulates a Homematic CCU (rfd, hmipserver, VirtualDevices, CUxD, ReGa) for automated tests",
|
|
5
5
|
"main": "sim.js",
|
|
6
|
+
"types": "./sim.d.ts",
|
|
6
7
|
"preferGlobal": true,
|
|
7
8
|
"bin": {
|
|
8
9
|
"hm-simulator": "./index.js"
|
|
@@ -13,9 +14,10 @@
|
|
|
13
14
|
"scripts": {
|
|
14
15
|
"lint": "eslint . && prettier --check .",
|
|
15
16
|
"format": "prettier --write . && eslint --fix .",
|
|
16
|
-
"test": "node --test test/*.test.js",
|
|
17
|
+
"test": "node --test test/*.test.js && npm run test:types",
|
|
17
18
|
"test:watch": "node --test --watch test/*.test.js",
|
|
18
|
-
"fixtures": "node tools/fixtures-from-paramsets.js"
|
|
19
|
+
"fixtures": "node tools/fixtures-from-paramsets.js",
|
|
20
|
+
"test:types": "tsc -p test/types"
|
|
19
21
|
},
|
|
20
22
|
"repository": {
|
|
21
23
|
"type": "git",
|
|
@@ -44,6 +46,8 @@
|
|
|
44
46
|
"index.js",
|
|
45
47
|
"sim.js",
|
|
46
48
|
"sim.mjs",
|
|
49
|
+
"sim.d.ts",
|
|
50
|
+
"sim.d.mts",
|
|
47
51
|
"lib/",
|
|
48
52
|
"data/",
|
|
49
53
|
"behaviors/",
|
|
@@ -57,9 +61,11 @@
|
|
|
57
61
|
},
|
|
58
62
|
"devDependencies": {
|
|
59
63
|
"@eslint/js": "^9.39.5",
|
|
64
|
+
"@types/node": "^20.19.43",
|
|
60
65
|
"eslint": "^9.39.5",
|
|
61
66
|
"eslint-config-prettier": "^10.1.8",
|
|
62
67
|
"globals": "^16.5.0",
|
|
63
|
-
"prettier": "^3.9.6"
|
|
68
|
+
"prettier": "^3.9.6",
|
|
69
|
+
"typescript": "^5.9.3"
|
|
64
70
|
}
|
|
65
71
|
}
|
package/sim.d.mts
ADDED
package/sim.d.ts
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
// Type declarations for hm-simulator. The package is plain CommonJS; these are written by hand and
|
|
2
|
+
// checked by `npm run test:types` (test/types/). Every option, scenario call or RPC method that is
|
|
3
|
+
// added to lib/sim.js is added here in the same change.
|
|
4
|
+
|
|
5
|
+
import type {EventEmitter} from 'node:events';
|
|
6
|
+
|
|
7
|
+
import type {FaultEntry, FaultName, FaultTable} from './lib/faults.js';
|
|
8
|
+
|
|
9
|
+
declare namespace HmSim {
|
|
10
|
+
/** The interfaces the simulator serves; `devices` may name others, which get no port. */
|
|
11
|
+
type InterfaceName = 'rfd' | 'hmip' | 'wired' | 'virtual' | 'cuxd' | (string & {});
|
|
12
|
+
|
|
13
|
+
/** A device or channel, as `listDevices` and `getDeviceDescription` answer it. */
|
|
14
|
+
interface DeviceDescription {
|
|
15
|
+
ADDRESS: string;
|
|
16
|
+
TYPE: string;
|
|
17
|
+
VERSION?: number;
|
|
18
|
+
PARENT?: string;
|
|
19
|
+
PARENT_TYPE?: string;
|
|
20
|
+
CHILDREN?: string[];
|
|
21
|
+
PARAMSETS?: string[];
|
|
22
|
+
FIRMWARE?: string;
|
|
23
|
+
AVAILABLE_FIRMWARE?: string;
|
|
24
|
+
FIRMWARE_UPDATE_STATE?: string;
|
|
25
|
+
UPDATABLE?: number | boolean;
|
|
26
|
+
FLAGS?: number;
|
|
27
|
+
INTERFACE?: string;
|
|
28
|
+
INDEX?: number;
|
|
29
|
+
DIRECTION?: number;
|
|
30
|
+
LINK_SOURCE_ROLES?: string;
|
|
31
|
+
LINK_TARGET_ROLES?: string;
|
|
32
|
+
RF_ADDRESS?: number;
|
|
33
|
+
ROAMING?: number;
|
|
34
|
+
RX_MODE?: number;
|
|
35
|
+
AES_ACTIVE?: number;
|
|
36
|
+
TEAM?: string;
|
|
37
|
+
TEAM_TAG?: string;
|
|
38
|
+
TEAM_CHANNELS?: string[];
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** One entry of `SPECIAL`: a value with a meaning of its own. */
|
|
43
|
+
interface SpecialValue {
|
|
44
|
+
ID: string;
|
|
45
|
+
VALUE: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** One parameter of a paramset description. */
|
|
49
|
+
interface ParameterDescription {
|
|
50
|
+
TYPE: 'BOOL' | 'ACTION' | 'INTEGER' | 'FLOAT' | 'ENUM' | 'STRING' | (string & {});
|
|
51
|
+
/** 1 read, 2 write, 4 event */
|
|
52
|
+
OPERATIONS: number;
|
|
53
|
+
/** 1 visible, 2 internal, 4 transform, 8 service, 16 sticky */
|
|
54
|
+
FLAGS?: number;
|
|
55
|
+
DEFAULT?: unknown;
|
|
56
|
+
MIN?: unknown;
|
|
57
|
+
MAX?: unknown;
|
|
58
|
+
VALUE_LIST?: string[];
|
|
59
|
+
UNIT?: string;
|
|
60
|
+
SPECIAL?: SpecialValue[];
|
|
61
|
+
ID?: string;
|
|
62
|
+
TAB_ORDER?: number;
|
|
63
|
+
CONTROL?: string;
|
|
64
|
+
[key: string]: unknown;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A paramset description: parameter name -> description. */
|
|
68
|
+
type ParamsetDescription = Record<string, ParameterDescription>;
|
|
69
|
+
|
|
70
|
+
/** A direct link. */
|
|
71
|
+
interface Link {
|
|
72
|
+
SENDER: string;
|
|
73
|
+
RECEIVER: string;
|
|
74
|
+
FLAGS?: number;
|
|
75
|
+
NAME?: string;
|
|
76
|
+
DESCRIPTION?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** What `listBidcosInterfaces` answers for one radio interface. */
|
|
80
|
+
interface BidcosInterface {
|
|
81
|
+
ADDRESS: string;
|
|
82
|
+
DESCRIPTION?: string;
|
|
83
|
+
CONNECTED?: boolean;
|
|
84
|
+
DEFAULT?: boolean;
|
|
85
|
+
FIRMWARE_VERSION?: string;
|
|
86
|
+
TYPE?: string;
|
|
87
|
+
DUTY_CYCLE?: number;
|
|
88
|
+
CARRIER_SENSE_LEVEL?: number;
|
|
89
|
+
[key: string]: unknown;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** `[address, datapoint, value]`, as `getServiceMessages` answers and events carry. */
|
|
93
|
+
type ServiceMessage = [address: string, datapoint: string, value: unknown];
|
|
94
|
+
type EventTuple = [address: string, datapoint: string, value: unknown];
|
|
95
|
+
|
|
96
|
+
interface Logger {
|
|
97
|
+
debug(...args: unknown[]): void;
|
|
98
|
+
info(...args: unknown[]): void;
|
|
99
|
+
warn(...args: unknown[]): void;
|
|
100
|
+
error(...args: unknown[]): void;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface BasicAuth {
|
|
104
|
+
username: string;
|
|
105
|
+
password: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** `true`: a generated self-signed certificate; `{key, cert}`: PEM; otherwise how to generate one. */
|
|
109
|
+
type TlsOption =
|
|
110
|
+
| boolean
|
|
111
|
+
| {key: string; cert: string}
|
|
112
|
+
| {commonName?: string; hostnames?: string[]; ips?: string[]; days?: number};
|
|
113
|
+
|
|
114
|
+
interface Config {
|
|
115
|
+
/** all interfaces unless set; `'127.0.0.1'` in tests */
|
|
116
|
+
listenAddress?: string;
|
|
117
|
+
/** rfd; 0 picks a free port */
|
|
118
|
+
binrpcListenPort?: number;
|
|
119
|
+
/** hmipserver */
|
|
120
|
+
xmlrpcListenPort?: number;
|
|
121
|
+
/** BidCos-Wired, started when `devices.wired` exists */
|
|
122
|
+
wiredListenPort?: number;
|
|
123
|
+
/** VirtualDevices, off unless set */
|
|
124
|
+
virtualListenPort?: number;
|
|
125
|
+
/** the path VirtualDevices answers on, `/groups` */
|
|
126
|
+
virtualPath?: string;
|
|
127
|
+
/** CUxD, off unless set */
|
|
128
|
+
cuxdListenPort?: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Behaviour per interface, `interfaces: {<iface>: {...}}`. */
|
|
132
|
+
interface InterfaceOptions {
|
|
133
|
+
configPendingMode?: 'hmip' | 'bidcos' | 'strict' | 'pending';
|
|
134
|
+
configPendingOnWrite?: boolean;
|
|
135
|
+
configPendingDelay?: number;
|
|
136
|
+
serviceMessagesEmptyAsString?: boolean;
|
|
137
|
+
getServiceMessagesFault?: boolean;
|
|
138
|
+
protocols?: Array<'binrpc' | 'xmlrpc'>;
|
|
139
|
+
listenerModel?: 'isolated' | 'measured';
|
|
140
|
+
deliveryTimeout?: number;
|
|
141
|
+
pong?: boolean;
|
|
142
|
+
pingDelay?: number;
|
|
143
|
+
startDelay?: number;
|
|
144
|
+
unreachWrites?: 'accept' | 'fault';
|
|
145
|
+
firmwareUpdateDelay?: number;
|
|
146
|
+
/** what `getVersion` answers */
|
|
147
|
+
version?: string;
|
|
148
|
+
/** BidCos only: what `getLGWStatus` answers; unknown method without it */
|
|
149
|
+
lgwStatus?: unknown;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The ReGa mock (`rega` option). */
|
|
153
|
+
interface RegaOptions {
|
|
154
|
+
port?: number;
|
|
155
|
+
listenAddress?: string;
|
|
156
|
+
variables?: unknown[];
|
|
157
|
+
/** devices and channels, the answer to `getChannels()` */
|
|
158
|
+
channels?: unknown[];
|
|
159
|
+
programs?: unknown[];
|
|
160
|
+
rooms?: unknown[];
|
|
161
|
+
functions?: unknown[];
|
|
162
|
+
/** the answer to `getValues()` */
|
|
163
|
+
values?: unknown[];
|
|
164
|
+
auth?: BasicAuth;
|
|
165
|
+
tls?: {key: string; cert: string};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
interface Options {
|
|
169
|
+
log?: Logger;
|
|
170
|
+
devices?: Record<string, {devices: DeviceDescription[]}>;
|
|
171
|
+
config?: Config;
|
|
172
|
+
/** directory with behaviour scripts, `false` for none */
|
|
173
|
+
behaviorPath?: string | false;
|
|
174
|
+
rega?: RegaOptions;
|
|
175
|
+
paramsetDescriptions?: Record<string, ParamsetDescription>;
|
|
176
|
+
paramsetFallback?: boolean;
|
|
177
|
+
faults?: Partial<Record<FaultName | (string & {}), Partial<FaultEntry>>>;
|
|
178
|
+
interfaces?: Partial<Record<InterfaceName, InterfaceOptions>>;
|
|
179
|
+
links?: Record<string, Link[]>;
|
|
180
|
+
bidcosInterfaces?: Record<string, BidcosInterface[]>;
|
|
181
|
+
serviceMessages?: Record<string, ServiceMessage[]>;
|
|
182
|
+
newDevices?: Record<string, {devices: DeviceDescription[]; delay?: number}>;
|
|
183
|
+
defaultRssi?: number;
|
|
184
|
+
/** `{<iface>: {<address>: {<key>: value}}}`, what `getMetadata` answers before any `setMetadata` */
|
|
185
|
+
metadata?: Record<string, Record<string, Record<string, unknown>>>;
|
|
186
|
+
tls?: TlsOption;
|
|
187
|
+
auth?: BasicAuth;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
interface WriteLogEntry {
|
|
191
|
+
iface: string;
|
|
192
|
+
address: string;
|
|
193
|
+
/** MASTER, VALUES, SERVICE or a peer address */
|
|
194
|
+
paramset: string;
|
|
195
|
+
values: Record<string, unknown>;
|
|
196
|
+
rejected?: Array<{name: string; value: unknown; reason: string; faultCode?: number}>;
|
|
197
|
+
method?: 'activateLinkParamset';
|
|
198
|
+
longPress?: boolean;
|
|
199
|
+
ts: number;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
interface CallbackLogEntry {
|
|
203
|
+
iface: string;
|
|
204
|
+
/** the url the client registered with */
|
|
205
|
+
client: string;
|
|
206
|
+
method: string;
|
|
207
|
+
sentAt: number;
|
|
208
|
+
answeredAt: number | null;
|
|
209
|
+
error: string | null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
interface ConfigPendingEntry {
|
|
213
|
+
address: string;
|
|
214
|
+
sticky: boolean;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
interface MissingParamsetDescription {
|
|
218
|
+
iface: string;
|
|
219
|
+
key: string;
|
|
220
|
+
usedKey: string | null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
interface InjectedFault {
|
|
224
|
+
iface?: string;
|
|
225
|
+
/** `'*'` for any */
|
|
226
|
+
method?: string;
|
|
227
|
+
/** `Infinity` or `-1` for all until `clearFaults()` */
|
|
228
|
+
times?: number;
|
|
229
|
+
delayMs?: number;
|
|
230
|
+
hang?: boolean;
|
|
231
|
+
closeSocket?: boolean;
|
|
232
|
+
fault?: FaultName | (string & {}) | {faultCode: number; faultString?: string};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
interface ScheduleStep {
|
|
236
|
+
at?: number;
|
|
237
|
+
call: string;
|
|
238
|
+
args?: unknown[];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
interface KeyMismatchScript {
|
|
242
|
+
/** the passphrase the device was taught in with */
|
|
243
|
+
key?: string;
|
|
244
|
+
/** its description and channel descriptions, paired once the temporary key is `key` */
|
|
245
|
+
devices?: DeviceDescription[];
|
|
246
|
+
/** milliseconds between the install mode and the device being heard */
|
|
247
|
+
delay?: number;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** An RPC fault as the simulator throws and answers it. */
|
|
251
|
+
interface RpcFault extends Error {
|
|
252
|
+
faultCode: number;
|
|
253
|
+
faultString: string;
|
|
254
|
+
detail?: string;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** `method -> handler(iface, params)`, as the servers dispatch it. */
|
|
258
|
+
type RpcMethodTable = Record<string, (iface: string, params: unknown[]) => unknown>;
|
|
259
|
+
|
|
260
|
+
/** The ports once `whenReady()` resolved. */
|
|
261
|
+
type Ports = Partial<Record<InterfaceName, number>>;
|
|
262
|
+
|
|
263
|
+
/** The ReGa mock. */
|
|
264
|
+
interface RegaSim {
|
|
265
|
+
port: number;
|
|
266
|
+
ready: Promise<void>;
|
|
267
|
+
/** every script it received */
|
|
268
|
+
scripts: string[];
|
|
269
|
+
/** every `dom.GetObject(id).Name("…")` among them */
|
|
270
|
+
renames: Array<{id: number; name: string; script: string}>;
|
|
271
|
+
variables: unknown[];
|
|
272
|
+
devices: unknown[];
|
|
273
|
+
programs: unknown[];
|
|
274
|
+
rooms: unknown[];
|
|
275
|
+
functions: unknown[];
|
|
276
|
+
values: unknown[];
|
|
277
|
+
close(): void;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** The emitter behaviour scripts get: `emit('setValue', iface, address, datapoint, value)`. */
|
|
281
|
+
interface BehaviorApi extends EventEmitter {
|
|
282
|
+
emit(event: 'setValue', iface: string, address: string, datapoint: string, value: unknown): boolean;
|
|
283
|
+
emit(event: string | symbol, ...args: unknown[]): boolean;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Simulates the RPC interface processes and the ReGa of a Homematic CCU.
|
|
289
|
+
*/
|
|
290
|
+
declare class HmSim {
|
|
291
|
+
constructor(options?: HmSim.Options);
|
|
292
|
+
|
|
293
|
+
readonly options: HmSim.Options;
|
|
294
|
+
log: HmSim.Logger;
|
|
295
|
+
config: HmSim.Config;
|
|
296
|
+
devices: Record<string, {devices: HmSim.DeviceDescription[]}>;
|
|
297
|
+
paramsetDescriptions: Record<string, HmSim.ParamsetDescription>;
|
|
298
|
+
paramsetFallback: boolean;
|
|
299
|
+
/** the effective fault table */
|
|
300
|
+
faults: FaultTable;
|
|
301
|
+
/** builds an {@link HmSim.RpcFault} from an entry of the fault table */
|
|
302
|
+
fault(name: FaultName | (string & {}), detail?: string): HmSim.RpcFault;
|
|
303
|
+
/** the port each server listens on, filled in once it listens */
|
|
304
|
+
ports: HmSim.Ports;
|
|
305
|
+
/** stored state per interface and address: `{VALUES, MASTER, LINKS, ...}` */
|
|
306
|
+
values: Record<string, Record<string, Record<string, Record<string, unknown>>>>;
|
|
307
|
+
links: Record<string, HmSim.Link[]>;
|
|
308
|
+
serviceMessages: Record<string, HmSim.ServiceMessage[]>;
|
|
309
|
+
/** every reportValueUsage */
|
|
310
|
+
valueUsage: Array<{iface: string; address: string; valueId: string; refCounter: number; ts: number}>;
|
|
311
|
+
/** every updateFirmware/installFirmware */
|
|
312
|
+
firmwareUpdates: Array<{iface: string; addresses: string[]; method: string; ts: number}>;
|
|
313
|
+
/** every refreshDeployedDeviceFirmwareList */
|
|
314
|
+
firmwareListRefreshes: Array<{iface: string; ts: number}>;
|
|
315
|
+
/** every changeKey */
|
|
316
|
+
keyChanges: Array<{iface: string; key: string; ts: number}>;
|
|
317
|
+
/** every setInterfaceClock */
|
|
318
|
+
interfaceClocks: Array<{iface: string; utc: number; offset: number; ts: number}>;
|
|
319
|
+
/** what setMetadata stored and the `metadata` option seeded */
|
|
320
|
+
metadata: Record<string, Record<string, Record<string, unknown>>>;
|
|
321
|
+
/** the serial getKeyMismatchDevice answers, per interface */
|
|
322
|
+
keyMismatch: Record<string, string>;
|
|
323
|
+
/** what scriptKeyMismatch() set up and did not pair yet */
|
|
324
|
+
keyMismatchScript: Record<string, Required<HmSim.KeyMismatchScript> & {serial: string}>;
|
|
325
|
+
/** the temporary key per interface, as setTempKey left it */
|
|
326
|
+
tempKey: Record<string, string>;
|
|
327
|
+
/** the log level per interface, as logLevel set it */
|
|
328
|
+
logLevels: Record<string, number>;
|
|
329
|
+
/** the RPC method table; a test may replace a handler */
|
|
330
|
+
rpcMethods: HmSim.RpcMethodTable;
|
|
331
|
+
/** the emitter behaviour scripts get */
|
|
332
|
+
api: HmSim.BehaviorApi;
|
|
333
|
+
/** the ReGa mock, when the `rega` option started it */
|
|
334
|
+
regaSim?: HmSim.RegaSim;
|
|
335
|
+
|
|
336
|
+
/** Resolves once every server accepts connections. */
|
|
337
|
+
whenReady(): Promise<void>;
|
|
338
|
+
/** Stops every server, timer and client connection. */
|
|
339
|
+
close(): void;
|
|
340
|
+
|
|
341
|
+
/* ---------------------------------------------------------------- scenario api: devices */
|
|
342
|
+
|
|
343
|
+
/** Pairs a device with its channels: `newDevices` to every client. Answers the ones that were new. */
|
|
344
|
+
addDevice(
|
|
345
|
+
iface: string,
|
|
346
|
+
...descriptions: Array<HmSim.DeviceDescription | HmSim.DeviceDescription[]>
|
|
347
|
+
): HmSim.DeviceDescription[];
|
|
348
|
+
addDevices(iface: string, list: HmSim.DeviceDescription[]): HmSim.DeviceDescription[];
|
|
349
|
+
/** Removes a device with its channels, values and links: `deleteDevices` to every client. */
|
|
350
|
+
removeDevice(iface: string, address: string): '';
|
|
351
|
+
/** The description, `false` when the interface does not know the address. */
|
|
352
|
+
getDevice(iface: string, address: string): HmSim.DeviceDescription | false;
|
|
353
|
+
/** Devices that appear the next time the install mode is switched on. */
|
|
354
|
+
scriptNewDevices(iface: string, devices: HmSim.DeviceDescription[], delay?: number): void;
|
|
355
|
+
/** A device that holds another system's key: heard at the next install mode, paired with its key. */
|
|
356
|
+
scriptKeyMismatch(iface: string, serial: string, script?: HmSim.KeyMismatchScript): void;
|
|
357
|
+
|
|
358
|
+
/* ---------------------------------------------------------------- scenario api: values and events */
|
|
359
|
+
|
|
360
|
+
/** One event, not checked against the description. Answers the value. */
|
|
361
|
+
fireEvent(iface: string, address: string, datapoint: string, value: unknown): unknown;
|
|
362
|
+
/** A burst of events as `system.multicall` batches. Answers how many. */
|
|
363
|
+
fireEvents(iface: string, events: HmSim.EventTuple[], options?: {batch?: number}): number;
|
|
364
|
+
/** A value, checked against the description; `internal` is the device reporting it. */
|
|
365
|
+
setValue(
|
|
366
|
+
iface: string,
|
|
367
|
+
address: string,
|
|
368
|
+
datapoint: string,
|
|
369
|
+
value: unknown,
|
|
370
|
+
options?: {strict?: boolean; internal?: boolean},
|
|
371
|
+
): void;
|
|
372
|
+
getValue(iface: string, address: string, datapoint: string): unknown;
|
|
373
|
+
getParamset(iface: string, address: string, key: string): Record<string, unknown>;
|
|
374
|
+
putParamset(iface: string, address: string, key: string, set: Record<string, unknown>): '';
|
|
375
|
+
getParamsetDescription(
|
|
376
|
+
iface: string,
|
|
377
|
+
device: string | HmSim.DeviceDescription,
|
|
378
|
+
paramset: string,
|
|
379
|
+
): HmSim.ParamsetDescription | undefined;
|
|
380
|
+
getDeviceDescription(iface: string, address: string): HmSim.DeviceDescription;
|
|
381
|
+
|
|
382
|
+
/* ---------------------------------------------------------------- scenario api: service messages and health */
|
|
383
|
+
|
|
384
|
+
/** Raises (or with a falsy value clears) a service message. */
|
|
385
|
+
setServiceMessage(iface: string, address: string, datapoint: string, value?: unknown): void;
|
|
386
|
+
getServiceMessages(iface: string): HmSim.ServiceMessage[] | '';
|
|
387
|
+
setConfigPending(iface: string, address: string, options?: {sticky?: boolean}): void;
|
|
388
|
+
clearConfigPending(iface: string, address: string): void;
|
|
389
|
+
setReachable(iface: string, address: string, reachable: boolean): void;
|
|
390
|
+
setLowBattery(iface: string, address: string, low: boolean): void;
|
|
391
|
+
setDutyCycle(iface: string, percent: number): void;
|
|
392
|
+
setCarrierSense(iface: string, percent: number): void;
|
|
393
|
+
offerFirmware(iface: string, address: string, version: string): void;
|
|
394
|
+
listBidcosInterfaces(iface: string): HmSim.BidcosInterface[];
|
|
395
|
+
|
|
396
|
+
/* ---------------------------------------------------------------- scenario api: interface processes */
|
|
397
|
+
|
|
398
|
+
dropConnection(iface: string): Promise<void>;
|
|
399
|
+
stopInterface(iface: string): Promise<void>;
|
|
400
|
+
startInterface(iface: string, options?: {forgetClients?: boolean}): Promise<void>;
|
|
401
|
+
restartInterface(iface: string, options?: {downMs?: number; forgetClients?: boolean}): Promise<void>;
|
|
402
|
+
injectFault(rule?: HmSim.InjectedFault): HmSim.InjectedFault;
|
|
403
|
+
clearFaults(): void;
|
|
404
|
+
/** Scenario calls on the simulator's timer; resolves with their results. */
|
|
405
|
+
schedule(steps: HmSim.ScheduleStep[]): Promise<unknown[]>;
|
|
406
|
+
|
|
407
|
+
/* ---------------------------------------------------------------- introspection */
|
|
408
|
+
|
|
409
|
+
getWriteLog(): HmSim.WriteLogEntry[];
|
|
410
|
+
getConfigPending(iface: string): HmSim.ConfigPendingEntry[];
|
|
411
|
+
getPoisonedChannels(iface: string): string[];
|
|
412
|
+
getMissingParamsetDescriptions(): HmSim.MissingParamsetDescription[];
|
|
413
|
+
getCallbackLog(): HmSim.CallbackLogEntry[];
|
|
414
|
+
getTempKey(iface: string): string;
|
|
415
|
+
getInstallMode(iface: string): number;
|
|
416
|
+
/** The names of every RPC method. */
|
|
417
|
+
methodNames(): string[];
|
|
418
|
+
methodHelp(name: string): string;
|
|
419
|
+
|
|
420
|
+
/* ---------------------------------------------------------------- rpc, as the servers call it */
|
|
421
|
+
|
|
422
|
+
/** One RPC method as a client call would run it; throws the fault. */
|
|
423
|
+
callMethod(iface: string, method: string, params?: unknown[]): unknown;
|
|
424
|
+
getLinks(iface: string, params?: unknown[]): Array<HmSim.Link & Record<string, unknown>>;
|
|
425
|
+
getLinkPeers(iface: string, params: unknown[]): string[];
|
|
426
|
+
deleteDevice(iface: string, address: string, flags?: number): '';
|
|
427
|
+
replaceDevice(iface: string, oldAddress: string, newAddress: string): '';
|
|
428
|
+
getKeyMismatchDevice(iface: string, params?: [reset?: boolean]): string;
|
|
429
|
+
suppressServiceMessages(
|
|
430
|
+
iface: string,
|
|
431
|
+
params: [channelAddress: string, parameter?: string, suppress?: boolean],
|
|
432
|
+
): '';
|
|
433
|
+
getSuppressedServiceMessages(iface: string, params: [channelAddress: string]): string[];
|
|
434
|
+
getMetadata(iface: string, params: [address: string, key: string]): unknown;
|
|
435
|
+
setMetadata(iface: string, params: [address: string, key: string, value: unknown]): '';
|
|
436
|
+
getAllMetadata(iface: string, params: [address: string]): Record<string, unknown>;
|
|
437
|
+
listReplaceableDevices(iface: string, params: [address: string]): HmSim.DeviceDescription[] | '';
|
|
438
|
+
getVersion(iface: string): string;
|
|
439
|
+
/** Sends one `event` to every client of the interface. */
|
|
440
|
+
event(iface: string, params: HmSim.EventTuple): void;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export = HmSim;
|