hm-simulator 1.3.0 → 1.3.2

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,35 @@
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.2
7
+
8
+ - `getParamsetDescription` of an address the interface does not know answers the `unknownInstance`
9
+ fault - `-2 Invalid device` under hmipserver's default table, `-2 Unknown instance` under rfd's -
10
+ as both interface processes do on 3.89.11; it answered `''`. A paramset the channel does not list
11
+ answers the new table entry `unknownParamsetDescription` (`-3 Unknown Paramset` / `-3 Unknown
12
+ paramset`, measured); a paramset the channel has but the description set lacks still answers `''`
13
+ (B-5, found through node-red-contrib-ccu's handling of the fault on a real CCU).
14
+ - `getLinkPeers` of a device address faults on `rfd` with the `notSupported` entry (`-1 Failure`
15
+ under rfd's table, as rfd 3.89.11 answers; `-1 Generic error` under the default one), where it
16
+ answered the peers of all channels; on hmipserver and the other interfaces it answers the channels'
17
+ lists one after the other, a peer linked with two of the channels twice, as hmipserver 3.89.11 does
18
+ (B-4).
19
+
20
+ ## 1.3.1
21
+
22
+ - `getLinks` without an address reports `FLAGS: 1` (`SENDER_BROKEN`) on every device-internal link of
23
+ `rfd` - a relay's own button on the relay, a dimmer's channel on its virtual channels - as rfd
24
+ 3.89.11 does although such a link works; filtered by an address the link carries its stored flags,
25
+ 0, as rfd answers there too. A test that shows a client how it handles rfd's flag can now make the
26
+ link with `addLink` instead of seeding it (B-3, found through Homematic Manager's links list).
27
+ - `replaceDevice` answers `true`, as the specification's `bool` says, where it answered `''` (a
28
+ client that takes the answer as the success saw a replacement that did happen as failed).
29
+ - rfd's `listReplaceableDevices` names only the devices of the same type that are unreachable
30
+ (`setReachable(iface, address, false)`); a device that still answers is not replaced.
31
+ - The declarations gain `clients` (the registered logic layers per interface, read-only, `Client`),
32
+ `dispatch` with its `DispatchContext` (the servers' entry, which a test may wrap to record calls),
33
+ and `replaceDevice`'s `true` (B-2, found through Homematic Manager's tests on 1.3.0).
34
+
6
35
  ## 1.3.0
7
36
 
8
37
  - The calls Homematic Manager makes, measured on rfd and hmipserver of firmware 3.89.11:
package/README.md CHANGED
@@ -487,6 +487,23 @@ Where the simulator imitates an interface process, this says whether the shape w
487
487
  CCU or is the simulator's model. The measurements were made for Homematic Manager (tasks 6 and 58)
488
488
  on CCUs with firmware 3.89.8 and 3.89.x.
489
489
 
490
+ ### Direct links
491
+
492
+ rfd (firmware 3.89.11, 2026-09-30) answers `getLinks("", flags)` - the list of all links - with
493
+ `FLAGS: 1` (`SENDER_BROKEN`) on every **device-internal** link: a relay's own button on the relay
494
+ (`:1` → `:1`), a dimmer's channel on its virtual channels. The link works; the bit carries no
495
+ information there. Asked for one address, `getLinks("ABC0000001:1", 0)` and `getLinks("ABC0000001",
496
+ 0)` answer the same link with `FLAGS: 0`, and `getLinkPeers` names the own channel once. The simulator
497
+ does the same on `rfd`: `addLink` stores `FLAGS: 0`, the unfiltered list adds bit 1 to internal links
498
+ (seeded flags are kept and or-ed), the filtered one reports what is stored. hmipserver is not
499
+ measured and reports the stored flags.
500
+
501
+ `getLinkPeers` of a **device** address (measured 2026-10-02, 3.89.11): rfd refuses it with `-1
502
+ Failure`; hmipserver answers the channels' peer lists one after the other, so a peer linked with two
503
+ of the channels is named twice. The simulator does the same: on `rfd` the `notSupported` entry of the
504
+ fault table in force (`-1 Generic error` under hmipserver's default table, `-1 Failure` under
505
+ `FAULT_TABLES.bidcos`), on every other interface the channels' lists in channel order.
506
+
490
507
  ### CONFIG_PENDING
491
508
 
492
509
  What a `putParamset MASTER` does, per interface (`interfaces.<iface>.configPendingMode`):
@@ -523,20 +540,21 @@ The two models are the explanations that competed before the measurement (Homema
523
540
  The default table is **hmipserver's**, because that is the one an application has to survive
524
541
  (measured):
525
542
 
526
- | key | code | string | when |
527
- | ------------------ | ---- | ------------------------------- | ---------------------------------------------------------------------------------- |
528
- | `unknownMethod` | -1 | Invalid XML-RPC message | no handler for the method name (hmipserver answers this without a faultCode) |
529
- | `unknownInstance` | -2 | Invalid device | address not known to this interface |
530
- | `unknownParamset` | -2 | Invalid device | the device has no such paramset |
531
- | `unknownLink` | -2 | Invalid device | the two channels are not linked |
532
- | `unknownParameter` | -5 | Unknown Parameter for value key | the paramset has no such parameter |
533
- | `readOnly` | -5 | Invalid parameter or value | `OPERATIONS & 2` is not set |
534
- | `typeError` | -5 | Invalid parameter or value | value does not fit the parameter's `TYPE` |
535
- | `outOfRange` | -5 | Invalid parameter or value | ENUM value not in `VALUE_LIST` |
536
- | `invalidValue` | -5 | Invalid parameter or value | the stored channel configuration cannot be transferred |
537
- | `notSupported` | -1 | Generic error | a method this interface does not implement |
538
- | `notReachable` | -1 | Generic error (UNREACH) | a sleeping battery device |
539
- | `invalidArguments` | -321 | Invalid arguments | wrong number of arguments; hmipserver really answers a Java exception message here |
543
+ | key | code | string | when |
544
+ | ---------------------------- | ---- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
545
+ | `unknownMethod` | -1 | Invalid XML-RPC message | no handler for the method name (hmipserver answers this without a faultCode) |
546
+ | `unknownInstance` | -2 | Invalid device | address not known to this interface |
547
+ | `unknownParamset` | -2 | Invalid device | `getParamset`/`putParamset` of a paramset the device does not have |
548
+ | `unknownParamsetDescription` | -3 | Unknown Paramset | `getParamsetDescription` of a paramset the channel does not list (3.89.11 appends the name: `Unknown Paramset: NOSUCH`) |
549
+ | `unknownLink` | -2 | Invalid device | the two channels are not linked |
550
+ | `unknownParameter` | -5 | Unknown Parameter for value key | the paramset has no such parameter |
551
+ | `readOnly` | -5 | Invalid parameter or value | `OPERATIONS & 2` is not set |
552
+ | `typeError` | -5 | Invalid parameter or value | value does not fit the parameter's `TYPE` |
553
+ | `outOfRange` | -5 | Invalid parameter or value | ENUM value not in `VALUE_LIST` |
554
+ | `invalidValue` | -5 | Invalid parameter or value | the stored channel configuration cannot be transferred |
555
+ | `notSupported` | -1 | Generic error | a method this interface does not implement |
556
+ | `notReachable` | -1 | Generic error (UNREACH) | a sleeping battery device |
557
+ | `invalidArguments` | -321 | Invalid arguments | wrong number of arguments; hmipserver really answers a Java exception message here |
540
558
 
541
559
  Over XML-RPC a fault is an XML-RPC fault, over BIN-RPC a message of type `0xff` with a
542
560
  `faultCode`/`faultString` struct. rfd's table is exported as `BIDCOS_FAULTS` and differs in more
@@ -549,6 +567,22 @@ new HmSim({faults: FAULT_TABLES.bidcos});
549
567
  new HmSim({faults: {unknownParameter: {faultCode: -4, faultString: 'Unknown parameter'}}}); // one entry
550
568
  ```
551
569
 
570
+ ### Unknown addresses and paramsets
571
+
572
+ Measured 2026-10-02 on rfd and hmipserver of firmware 3.89.11, with a device address and a channel
573
+ address alike: an address the interface does not know is a fault from every method that takes one -
574
+ `getDeviceDescription`, `getParamsetDescription`, `getParamset`, `getLinks`, `getLinkPeers` - rfd's
575
+ `-2 Unknown instance`, hmipserver's `-2 Invalid device`. The simulator answers `unknownInstance` of
576
+ the table in force everywhere (until 1.3.1 `getParamsetDescription` answered `''` there).
577
+
578
+ `getParamsetDescription` of a paramset the channel does not have: hmipserver answers `-3 Unknown
579
+ Paramset: <name>`; rfd answers `-3 Unknown paramset` on a channel without a `LINK` paramset and the
580
+ `LINK` description on a channel that has one (the name is taken as a peer address, as `getParamset`
581
+ does). The simulator answers `unknownParamsetDescription` whenever the channel's `PARAMSETS` lacks
582
+ the name. A paramset the channel lists but the description set has no entry for still answers `''`:
583
+ that is a gap in the fixture (logged once, see
584
+ [Device data and fixtures](#device-data-and-fixtures)), not something an interface process does.
585
+
552
586
  ### Service messages
553
587
 
554
588
  `getServiceMessages` answers `[[address, datapoint, value]]`: every datapoint whose description
@@ -609,7 +643,8 @@ lifted again):
609
643
 
610
644
  The simulator answers so, with the fault table in force (`-1 Generic error` under hmipserver's
611
645
  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
646
+ `listReplaceableDevices` on rfd lists the other devices of the same `TYPE` that are unreachable
647
+ (`setReachable(iface, address, false)`; a device that still answers is not replaced); `setMetadata` on rfd
613
648
  knows only its own addresses; `setInterfaceClock` is recorded, not measured (it sets the clock the
614
649
  devices get). `addDevice(serial)` pairs only the device of `scriptKeyMismatch`, with the right
615
650
  temporary key; any other serial is `unknownInstance`, the mismatch a `notSupported` fault.
package/lib/faults.d.ts CHANGED
@@ -11,6 +11,7 @@ export type FaultName =
11
11
  | 'unknownMethod'
12
12
  | 'unknownInstance'
13
13
  | 'unknownParamset'
14
+ | 'unknownParamsetDescription'
14
15
  | 'unknownParameter'
15
16
  | 'unknownLink'
16
17
  | 'readOnly'
package/lib/faults.js CHANGED
@@ -35,8 +35,11 @@ const DEFAULT_FAULTS = {
35
35
  unknownMethod: {faultCode: -1, faultString: 'Invalid XML-RPC message'},
36
36
  // device or channel address not known to this interface process
37
37
  unknownInstance: {faultCode: -2, faultString: 'Invalid device'},
38
- // the device/channel exists but does not have the requested paramset
38
+ // the device/channel exists but does not have the requested paramset (getParamset, putParamset)
39
39
  unknownParamset: {faultCode: -2, faultString: 'Invalid device'},
40
+ // getParamsetDescription of a paramset the channel does not have. hmipserver 3.89.11 appends the
41
+ // name it was asked for (`Unknown Paramset: NOSUCH`); measured 2026-10-02
42
+ unknownParamsetDescription: {faultCode: -3, faultString: 'Unknown Paramset'},
40
43
  // the paramset exists but does not contain the requested parameter (getValue/setValue)
41
44
  unknownParameter: {faultCode: -5, faultString: 'Unknown Parameter for value key'},
42
45
  // the two channels are not linked with each other
@@ -68,6 +71,10 @@ const BIDCOS_FAULTS = {
68
71
  // rfd does not fault here at all: an unknown paramset name is taken as a peer channel address
69
72
  // and the link defaults come back. The entry covers what the simulator cannot model.
70
73
  unknownParamset: {faultCode: -2, faultString: 'Unknown instance'},
74
+ // getParamsetDescription of a paramset the channel does not have, on a channel without a LINK
75
+ // paramset; a channel that has one answers the LINK description for any unknown name (rfd
76
+ // 3.89.11, measured 2026-10-02)
77
+ unknownParamsetDescription: {faultCode: -3, faultString: 'Unknown paramset'},
71
78
  unknownParameter: {faultCode: -5, faultString: 'Unknown parameter'},
72
79
  unknownLink: {faultCode: -1, faultString: 'Failure'},
73
80
  // rfd accepts a setValue on a read-only datapoint, so this is never raised against it
package/lib/sim.js CHANGED
@@ -45,6 +45,15 @@ const CALLBACK_LOG_SIZE = 10000;
45
45
  /** the interfaces that speak the BidCos protocol, and therefore have setTempKey */
46
46
  const BIDCOS_INTERFACES = new Set(['rfd', 'wired']);
47
47
 
48
+ /** FLAGS bit of a link whose sender rfd counts as broken (bit 2 is the receiver) */
49
+ const LINK_SENDER_BROKEN = 1;
50
+
51
+ /** The device part of a channel address: `ABC0000001:1` -> `ABC0000001`. */
52
+ function deviceOf(address) {
53
+ const colon = String(address).indexOf(':');
54
+ return colon === -1 ? String(address) : String(address).slice(0, colon);
55
+ }
56
+
48
57
  /** device types of the HmIP radio modules, which carry DUTY_CYCLE_LEVEL and CARRIER_SENSE_LEVEL */
49
58
  const RADIO_MODULE_TYPES = new Set(['RPI-RF-MOD', 'HmIP-RFUSB', 'HmIP-RFUSB-TK', 'HMIP-CCU3', 'HmIP-CCU3']);
50
59
 
@@ -562,6 +571,28 @@ class HmSim {
562
571
  return this.requireDevice(iface, address);
563
572
  }
564
573
 
574
+ /**
575
+ * The RPC `getParamsetDescription`: unlike {@link getParamsetDescription}, which the simulator
576
+ * calls for itself, an address the interface does not know is a fault (rfd `-2 Unknown instance`,
577
+ * hmipserver `-2 Invalid device`, measured on 3.89.11), and so is a paramset the channel does not
578
+ * list in `PARAMSETS` (`-3` on both). A paramset the channel has but the description set lacks
579
+ * still answers `''`, logged once by {@link resolveParamsetKey} - that is a gap in the fixture,
580
+ * not something an interface process does.
581
+ * @param {string} iface
582
+ * @param {string} address device or channel address
583
+ * @param {string} paramset MASTER, VALUES, LINK or SERVICE
584
+ */
585
+ describeParamset(iface, address, paramset) {
586
+ const device = this.requireDevice(iface, address);
587
+ if (typeof paramset !== 'string' || paramset === '') {
588
+ throw this.fault('invalidArguments', 'paramset type');
589
+ }
590
+ if (Array.isArray(device.PARAMSETS) && !device.PARAMSETS.includes(paramset)) {
591
+ throw this.fault('unknownParamsetDescription', `${address}/${paramset}`);
592
+ }
593
+ return this.getParamsetDescription(iface, device, paramset);
594
+ }
595
+
565
596
  getValue(iface, address, datapoint) {
566
597
  const {description, values} = this.storedParamset(iface, address, 'VALUES');
567
598
  if (!description[datapoint]) {
@@ -897,14 +928,24 @@ class HmSim {
897
928
  links = links.filter((link) => addresses.has(link.SENDER) || addresses.has(link.RECEIVER));
898
929
  }
899
930
 
900
- return links.map((link) => this.linkStruct(iface, link, flags));
931
+ // rfd (measured, firmware 3.89.11): the list of *all* links carries FLAGS bit 1 (SENDER_BROKEN)
932
+ // on every device-internal link - a relay's own button on the relay, a dimmer's channel on its
933
+ // virtual channels - although the link works; asked for one address, the same link comes with
934
+ // its stored flags, 0. The simulator does the same on rfd, whatever `flags` asks for.
935
+ const internalBroken = iface === 'rfd' && !address;
936
+ return links.map((link) => this.linkStruct(iface, link, flags, internalBroken));
937
+ }
938
+
939
+ /** Sender and receiver on one device: `ABC0000001:1` and `ABC0000001:2`, or the same channel. */
940
+ isInternalLink(link) {
941
+ return deviceOf(link.SENDER) === deviceOf(link.RECEIVER);
901
942
  }
902
943
 
903
- linkStruct(iface, link, flags) {
944
+ linkStruct(iface, link, flags, internalBroken = false) {
904
945
  const result = {
905
946
  SENDER: link.SENDER,
906
947
  RECEIVER: link.RECEIVER,
907
- FLAGS: link.FLAGS || 0,
948
+ FLAGS: (link.FLAGS || 0) | (internalBroken && this.isInternalLink(link) ? LINK_SENDER_BROKEN : 0),
908
949
  NAME: link.NAME || '',
909
950
  DESCRIPTION: link.DESCRIPTION || '',
910
951
  };
@@ -930,14 +971,31 @@ class HmSim {
930
971
  }
931
972
  }
932
973
 
974
+ /**
975
+ * The peers of a channel: the other end of every link it is on, the own channel once for a link
976
+ * with itself. For a device address rfd answers `-1 Failure` (measured, 3.89.11); hmipserver
977
+ * answers the channels' lists one after the other, a peer linked with two of the channels twice
978
+ * (measured, 3.89.11) - the simulator does the same on every interface but `rfd`.
979
+ * @param {string} iface
980
+ * @param {Array} params [address]
981
+ * @returns {string[]} channel addresses
982
+ */
933
983
  getLinkPeers(iface, params) {
934
- const addresses = new Set(this.channelsOf(iface, params[0]));
984
+ const [address] = params || [];
985
+ const device = this.requireDevice(iface, address);
986
+ if (iface === 'rfd' && !device.PARENT) {
987
+ // rfd's -1 Failure; -1 Generic error under hmipserver's default table
988
+ throw this.fault('notSupported', `getLinkPeers of a device address: ${address}`);
989
+ }
990
+
935
991
  const peers = [];
936
- for (const link of this.links[iface] || []) {
937
- if (addresses.has(link.SENDER)) {
938
- peers.push(link.RECEIVER);
939
- } else if (addresses.has(link.RECEIVER)) {
940
- peers.push(link.SENDER);
992
+ for (const channel of this.channelsOf(iface, address)) {
993
+ for (const link of this.links[iface] || []) {
994
+ if (link.SENDER === channel) {
995
+ peers.push(link.RECEIVER);
996
+ } else if (link.RECEIVER === channel) {
997
+ peers.push(link.SENDER);
998
+ }
941
999
  }
942
1000
  }
943
1001
  return peers;
@@ -1373,9 +1431,11 @@ class HmSim {
1373
1431
 
1374
1432
  /**
1375
1433
  * `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 `''`.
1434
+ * devices of the same `TYPE` that are unreachable (`setReachable(iface, address, false)`) - a
1435
+ * device that still answers is not replaced (the simulator's model, after what the CCU's WebUI
1436
+ * offers; measured: `[]` for rfd's own `BidCoS-RF`, `-2 Unknown instance` for an address it does
1437
+ * not know). hmipserver answers with an empty HTTP body whatever it is asked (measured, firmware
1438
+ * 3.89.11), which the simulator answers as `''`.
1379
1439
  */
1380
1440
  listReplaceableDevices(iface, params) {
1381
1441
  const [address] = params || [];
@@ -1389,7 +1449,10 @@ class HmSim {
1389
1449
  if (device.PARENT) {
1390
1450
  return [];
1391
1451
  }
1392
- return this.deviceList(iface).filter((entry) => entry.TYPE === device.TYPE && entry.ADDRESS !== address);
1452
+ const unreachable = this.unreachable[iface] || new Set();
1453
+ return this.deviceList(iface).filter(
1454
+ (entry) => entry.TYPE === device.TYPE && entry.ADDRESS !== address && unreachable.has(entry.ADDRESS),
1455
+ );
1393
1456
  }
1394
1457
 
1395
1458
  /**
@@ -1736,7 +1799,7 @@ class HmSim {
1736
1799
  }
1737
1800
 
1738
1801
  this.deleteDevice(iface, oldAddress);
1739
- return '';
1802
+ return true;
1740
1803
  }
1741
1804
 
1742
1805
  reportValueUsage(iface, params) {
@@ -2392,7 +2455,7 @@ class HmSim {
2392
2455
  'system.multicall': (iface, params) => this.multicall(iface, params),
2393
2456
  init: (iface, params) => this.init(iface, params),
2394
2457
  listDevices: (iface) => this.devices[iface].devices,
2395
- getParamsetDescription: (iface, params) => this.getParamsetDescription(iface, params[0], params[1]),
2458
+ getParamsetDescription: (iface, params) => this.describeParamset(iface, params[0], params[1]),
2396
2459
  getDeviceDescription: (iface, params) => this.getDeviceDescription(iface, params[0]),
2397
2460
  ping: (iface, params) => this.ping(iface, params),
2398
2461
  setValue: (iface, params) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hm-simulator",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Simulates a Homematic CCU (rfd, hmipserver, VirtualDevices, CUxD, ReGa) for automated tests",
5
5
  "main": "sim.js",
6
6
  "types": "./sim.d.ts",
package/sim.d.ts CHANGED
@@ -257,6 +257,31 @@ declare namespace HmSim {
257
257
  /** `method -> handler(iface, params)`, as the servers dispatch it. */
258
258
  type RpcMethodTable = Record<string, (iface: string, params: unknown[]) => unknown>;
259
259
 
260
+ /** A logic layer registered with `init`: what the simulator calls back. */
261
+ interface Client {
262
+ /** the interface id the client gave `init` */
263
+ id: string;
264
+ /** its callback url, as given */
265
+ url: string;
266
+ /** the key in `clients[iface]` (the url, exactly) */
267
+ key: string;
268
+ /** the underlying binrpc or xmlrpc client */
269
+ client: unknown;
270
+ /** one callback to the client; logged in `getCallbackLog()` */
271
+ methodCall(
272
+ method: string,
273
+ params: unknown[],
274
+ callback: (error: Error | null | undefined, result?: unknown) => void,
275
+ options?: {timeout?: number},
276
+ ): void;
277
+ }
278
+
279
+ /** What the servers hand `dispatch` besides the call. */
280
+ interface DispatchContext {
281
+ /** the connection the call came in on, for an injected `closeSocket` */
282
+ socket?: {destroy(): void};
283
+ }
284
+
260
285
  /** The ports once `whenReady()` resolved. */
261
286
  type Ports = Partial<Record<InterfaceName, number>>;
262
287
 
@@ -305,6 +330,8 @@ declare class HmSim {
305
330
  /** stored state per interface and address: `{VALUES, MASTER, LINKS, ...}` */
306
331
  values: Record<string, Record<string, Record<string, Record<string, unknown>>>>;
307
332
  links: Record<string, HmSim.Link[]>;
333
+ /** the registered logic layers per interface, by callback url: the subscriptions `init(url, '')` removes */
334
+ readonly clients: Record<string, Record<string, HmSim.Client>>;
308
335
  serviceMessages: Record<string, HmSim.ServiceMessage[]>;
309
336
  /** every reportValueUsage */
310
337
  valueUsage: Array<{iface: string; address: string; valueId: string; refCounter: number; ts: number}>;
@@ -378,6 +405,8 @@ declare class HmSim {
378
405
  paramset: string,
379
406
  ): HmSim.ParamsetDescription | undefined;
380
407
  getDeviceDescription(iface: string, address: string): HmSim.DeviceDescription;
408
+ /** the RPC `getParamsetDescription`: faults for an unknown address or a paramset the channel lacks */
409
+ describeParamset(iface: string, address: string, paramset: string): HmSim.ParamsetDescription | undefined;
381
410
 
382
411
  /* ---------------------------------------------------------------- scenario api: service messages and health */
383
412
 
@@ -421,10 +450,21 @@ declare class HmSim {
421
450
 
422
451
  /** One RPC method as a client call would run it; throws the fault. */
423
452
  callMethod(iface: string, method: string, params?: unknown[]): unknown;
453
+ /**
454
+ * The servers' entry for every incoming call: injected faults and the lifecycle gates apply, then
455
+ * `callMethod`, and `callback(fault)` or `callback(null, result)`. A test wraps it to record calls.
456
+ */
457
+ dispatch(
458
+ iface: string,
459
+ method: string,
460
+ params: unknown[],
461
+ callback: (fault: HmSim.RpcFault | null | undefined, result?: unknown) => void,
462
+ context?: HmSim.DispatchContext,
463
+ ): void;
424
464
  getLinks(iface: string, params?: unknown[]): Array<HmSim.Link & Record<string, unknown>>;
425
465
  getLinkPeers(iface: string, params: unknown[]): string[];
426
466
  deleteDevice(iface: string, address: string, flags?: number): '';
427
- replaceDevice(iface: string, oldAddress: string, newAddress: string): '';
467
+ replaceDevice(iface: string, oldAddress: string, newAddress: string): true;
428
468
  getKeyMismatchDevice(iface: string, params?: [reset?: boolean]): string;
429
469
  suppressServiceMessages(
430
470
  iface: string,