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 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 | `init` (register and de-register a logic layer), `ping`, `system.listMethods`, `system.methodHelp`, `system.multicall` |
124
- | devices | `listDevices`, `getDeviceDescription`, `deleteDevice`, `replaceDevice`, `setInstallMode`, `getInstallMode` |
125
- | paramsets | `getParamsetDescription`, `getParamset`, `putParamset`, `getValue`, `setValue`, `determineParameter`, `reportValueUsage` |
126
- | links | `getLinks`, `getLinkPeers`, `getLinkInfo`, `setLinkInfo`, `addLink`, `removeLink`, `activateLinkParamset` |
127
- | interface | `rssiInfo`, `listBidcosInterfaces`, `setBidcosInterface`, `getServiceMessages`, `setTempKey` (BidCos only), `changeKey` (rfd, hmipserver), `logLevel` |
128
- | teams | `listTeams`, `setTeam` (BidCos only): the smoke detector teams as rfd keeps them, pseudo devices `*<serial>` |
129
- | 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`, `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
- | `config.listenAddress` | all interfaces | `'127.0.0.1'` in tests |
193
- | `config.binrpcListenPort` | – | rfd |
194
- | `config.xmlrpcListenPort` | – | hmipserver |
195
- | `config.wiredListenPort` | – | BidCos-Wired, started when `devices.wired` exists |
196
- | `config.virtualListenPort` | off | VirtualDevices |
197
- | `config.virtualPath` | `/groups` | the path VirtualDevices answers on |
198
- | `config.cuxdListenPort` | off | CUxD |
199
- | `interfaces` | see below | behaviour per interface, `{<iface>: {...}}` |
200
- | `faults` | hmipserver's table | overrides for the fault table, see [Fault codes](#fault-codes) |
201
- | `behaviorPath` | `behaviors/` of the package | directory with [behaviour scripts](#behaviour-scripts), `false` for none |
202
- | `rega` | off | the [ReGa mock](#rega-mock) |
203
- | `tls` | off | `true` or `{key, cert}`, see [TLS and basic auth](#tls-and-basic-auth) |
204
- | `auth` | off | `{username, password}`, HTTP basic auth |
205
- | `log` | silent | an object with `debug`, `info`, `warn`, `error` (`console` will do) |
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
@@ -14,6 +14,7 @@ const SCENARIO_METHODS = new Set([
14
14
  'setValue',
15
15
  'setServiceMessage',
16
16
  'scriptNewDevices',
17
+ 'scriptKeyMismatch',
17
18
  'dropConnection',
18
19
  'stopInterface',
19
20
  'startInterface',
@@ -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
+ };
@@ -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
- wired: {configPendingMode: 'bidcos'},
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
- return values[datapoint] === undefined ? defaultValue(description[datapoint]) : values[datapoint];
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
- return {...values};
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) => [address, name, state.VALUES[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.2.0",
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
@@ -0,0 +1,5 @@
1
+ // Type declarations for sim.mjs, the ESM entry point.
2
+ import HmSim from './sim.js';
3
+
4
+ export default HmSim;
5
+ export {HmSim};
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;