hm-simulator 1.3.0 → 1.3.1

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,21 @@
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.1
7
+
8
+ - `getLinks` without an address reports `FLAGS: 1` (`SENDER_BROKEN`) on every device-internal link of
9
+ `rfd` - a relay's own button on the relay, a dimmer's channel on its virtual channels - as rfd
10
+ 3.89.11 does although such a link works; filtered by an address the link carries its stored flags,
11
+ 0, as rfd answers there too. A test that shows a client how it handles rfd's flag can now make the
12
+ link with `addLink` instead of seeding it (B-3, found through Homematic Manager's links list).
13
+ - `replaceDevice` answers `true`, as the specification's `bool` says, where it answered `''` (a
14
+ client that takes the answer as the success saw a replacement that did happen as failed).
15
+ - rfd's `listReplaceableDevices` names only the devices of the same type that are unreachable
16
+ (`setReachable(iface, address, false)`); a device that still answers is not replaced.
17
+ - The declarations gain `clients` (the registered logic layers per interface, read-only, `Client`),
18
+ `dispatch` with its `DispatchContext` (the servers' entry, which a test may wrap to record calls),
19
+ and `replaceDevice`'s `true` (B-2, found through Homematic Manager's tests on 1.3.0).
20
+
6
21
  ## 1.3.0
7
22
 
8
23
  - The calls Homematic Manager makes, measured on rfd and hmipserver of firmware 3.89.11:
package/README.md CHANGED
@@ -487,6 +487,18 @@ 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. rfd also refuses `getLinkPeers` of a device address (`-1
500
+ Failure`); the simulator answers the peers of all its channels.
501
+
490
502
  ### CONFIG_PENDING
491
503
 
492
504
  What a `putParamset MASTER` does, per interface (`interfaces.<iface>.configPendingMode`):
@@ -609,7 +621,8 @@ lifted again):
609
621
 
610
622
  The simulator answers so, with the fault table in force (`-1 Generic error` under hmipserver's
611
623
  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
624
+ `listReplaceableDevices` on rfd lists the other devices of the same `TYPE` that are unreachable
625
+ (`setReachable(iface, address, false)`; a device that still answers is not replaced); `setMetadata` on rfd
613
626
  knows only its own addresses; `setInterfaceClock` is recorded, not measured (it sets the clock the
614
627
  devices get). `addDevice(serial)` pairs only the device of `scriptKeyMismatch`, with the right
615
628
  temporary key; any other serial is `unknownInstance`, the mismatch a `notSupported` fault.
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
 
@@ -897,14 +906,24 @@ class HmSim {
897
906
  links = links.filter((link) => addresses.has(link.SENDER) || addresses.has(link.RECEIVER));
898
907
  }
899
908
 
900
- return links.map((link) => this.linkStruct(iface, link, flags));
909
+ // rfd (measured, firmware 3.89.11): the list of *all* links carries FLAGS bit 1 (SENDER_BROKEN)
910
+ // on every device-internal link - a relay's own button on the relay, a dimmer's channel on its
911
+ // virtual channels - although the link works; asked for one address, the same link comes with
912
+ // its stored flags, 0. The simulator does the same on rfd, whatever `flags` asks for.
913
+ const internalBroken = iface === 'rfd' && !address;
914
+ return links.map((link) => this.linkStruct(iface, link, flags, internalBroken));
915
+ }
916
+
917
+ /** Sender and receiver on one device: `ABC0000001:1` and `ABC0000001:2`, or the same channel. */
918
+ isInternalLink(link) {
919
+ return deviceOf(link.SENDER) === deviceOf(link.RECEIVER);
901
920
  }
902
921
 
903
- linkStruct(iface, link, flags) {
922
+ linkStruct(iface, link, flags, internalBroken = false) {
904
923
  const result = {
905
924
  SENDER: link.SENDER,
906
925
  RECEIVER: link.RECEIVER,
907
- FLAGS: link.FLAGS || 0,
926
+ FLAGS: (link.FLAGS || 0) | (internalBroken && this.isInternalLink(link) ? LINK_SENDER_BROKEN : 0),
908
927
  NAME: link.NAME || '',
909
928
  DESCRIPTION: link.DESCRIPTION || '',
910
929
  };
@@ -1373,9 +1392,11 @@ class HmSim {
1373
1392
 
1374
1393
  /**
1375
1394
  * `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 `''`.
1395
+ * devices of the same `TYPE` that are unreachable (`setReachable(iface, address, false)`) - a
1396
+ * device that still answers is not replaced (the simulator's model, after what the CCU's WebUI
1397
+ * offers; measured: `[]` for rfd's own `BidCoS-RF`, `-2 Unknown instance` for an address it does
1398
+ * not know). hmipserver answers with an empty HTTP body whatever it is asked (measured, firmware
1399
+ * 3.89.11), which the simulator answers as `''`.
1379
1400
  */
1380
1401
  listReplaceableDevices(iface, params) {
1381
1402
  const [address] = params || [];
@@ -1389,7 +1410,10 @@ class HmSim {
1389
1410
  if (device.PARENT) {
1390
1411
  return [];
1391
1412
  }
1392
- return this.deviceList(iface).filter((entry) => entry.TYPE === device.TYPE && entry.ADDRESS !== address);
1413
+ const unreachable = this.unreachable[iface] || new Set();
1414
+ return this.deviceList(iface).filter(
1415
+ (entry) => entry.TYPE === device.TYPE && entry.ADDRESS !== address && unreachable.has(entry.ADDRESS),
1416
+ );
1393
1417
  }
1394
1418
 
1395
1419
  /**
@@ -1736,7 +1760,7 @@ class HmSim {
1736
1760
  }
1737
1761
 
1738
1762
  this.deleteDevice(iface, oldAddress);
1739
- return '';
1763
+ return true;
1740
1764
  }
1741
1765
 
1742
1766
  reportValueUsage(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.1",
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}>;
@@ -421,10 +448,21 @@ declare class HmSim {
421
448
 
422
449
  /** One RPC method as a client call would run it; throws the fault. */
423
450
  callMethod(iface: string, method: string, params?: unknown[]): unknown;
451
+ /**
452
+ * The servers' entry for every incoming call: injected faults and the lifecycle gates apply, then
453
+ * `callMethod`, and `callback(fault)` or `callback(null, result)`. A test wraps it to record calls.
454
+ */
455
+ dispatch(
456
+ iface: string,
457
+ method: string,
458
+ params: unknown[],
459
+ callback: (fault: HmSim.RpcFault | null | undefined, result?: unknown) => void,
460
+ context?: HmSim.DispatchContext,
461
+ ): void;
424
462
  getLinks(iface: string, params?: unknown[]): Array<HmSim.Link & Record<string, unknown>>;
425
463
  getLinkPeers(iface: string, params: unknown[]): string[];
426
464
  deleteDevice(iface: string, address: string, flags?: number): '';
427
- replaceDevice(iface: string, oldAddress: string, newAddress: string): '';
465
+ replaceDevice(iface: string, oldAddress: string, newAddress: string): true;
428
466
  getKeyMismatchDevice(iface: string, params?: [reset?: boolean]): string;
429
467
  suppressServiceMessages(
430
468
  iface: string,