hm-simulator 1.2.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/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
 
@@ -113,6 +122,9 @@ const INTERFACE_DEFAULTS = {
113
122
  // rfd answers the empty string instead of an empty array when it has no service message; the
114
123
  // real shape, off by default because it breaks every consumer that assumes an array
115
124
  serviceMessagesEmptyAsString: false,
125
+ // answer getServiceMessages with the unknown-method fault, as VirtualDevices, CUxD and
126
+ // hmipserver were seen to on 3.89.x ("Invalid XML-RPC message"); off by default
127
+ getServiceMessagesFault: false,
116
128
  };
117
129
 
118
130
  /**
@@ -121,11 +133,15 @@ const INTERFACE_DEFAULTS = {
121
133
  */
122
134
  const MEASURED_INTERFACE_DEFAULTS = {
123
135
  // 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'},
136
+ hmip: {configPendingMode: 'hmip', pong: false, version: '3.89.11.20260919'},
137
+ rfd: {configPendingMode: 'bidcos', version: '2.6.0'},
138
+ // hs485d's getVersion was not measured; rfd's answer stands in
139
+ wired: {configPendingMode: 'bidcos', version: '2.6.0'},
127
140
  };
128
141
 
142
+ /** the interfaces with the metadata methods: rfd and hs485d fault on a key that is not set, hmipserver answers '' */
143
+ const METADATA_INTERFACES = new Set(['rfd', 'wired', 'hmip']);
144
+
129
145
  /** what listBidcosInterfaces answers when the `bidcosInterfaces` option does not say otherwise */
130
146
  const DEFAULT_BIDCOS_INTERFACES = {
131
147
  rfd: [
@@ -265,6 +281,16 @@ class HmSim {
265
281
  this.unreachable = {};
266
282
  /** what listBidcosInterfaces reports on top of the configured answer: {<iface>: {DUTY_CYCLE, ...}} */
267
283
  this.radio = {};
284
+ /** a device that holds another system's key: {<iface>: {serial, key, devices, delay}}, see scriptKeyMismatch() */
285
+ this.keyMismatchScript = {};
286
+ /** the serial getKeyMismatchDevice answers: {<iface>: serial} */
287
+ this.keyMismatch = {};
288
+ /** suppressed service messages (hmipserver): {<iface>: Map<channelAddress, Set<parameter>>} */
289
+ this.suppressed = {};
290
+ /** metadata per interface and address: {<iface>: {<address>: {<key>: value}}} */
291
+ this.metadata = structuredClone(options.metadata || {});
292
+ /** every setInterfaceClock call: [{iface, utc, offset, ts}] */
293
+ this.interfaceClocks = [];
268
294
 
269
295
  this.rpcMethods = this.buildRpcMethods();
270
296
 
@@ -550,7 +576,8 @@ class HmSim {
550
576
  if (!description[datapoint]) {
551
577
  throw this.fault('unknownParameter', `${address}/${datapoint}`);
552
578
  }
553
- return values[datapoint] === undefined ? defaultValue(description[datapoint]) : values[datapoint];
579
+ const value = values[datapoint] === undefined ? defaultValue(description[datapoint]) : values[datapoint];
580
+ return this.reportedValue(iface, address, datapoint, value);
554
581
  }
555
582
 
556
583
  /**
@@ -563,7 +590,14 @@ class HmSim {
563
590
  throw this.fault('invalidArguments', 'paramset key');
564
591
  }
565
592
  const {values} = this.storedParamset(iface, address, key);
566
- return {...values};
593
+ if (key !== 'VALUES' || !this.suppressed[iface] || !this.suppressed[iface].has(address)) {
594
+ return {...values};
595
+ }
596
+ const reported = {};
597
+ for (const [name, value] of Object.entries(values)) {
598
+ reported[name] = this.reportedValue(iface, address, name, value);
599
+ }
600
+ return reported;
567
601
  }
568
602
 
569
603
  /**
@@ -872,14 +906,24 @@ class HmSim {
872
906
  links = links.filter((link) => addresses.has(link.SENDER) || addresses.has(link.RECEIVER));
873
907
  }
874
908
 
875
- 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);
876
920
  }
877
921
 
878
- linkStruct(iface, link, flags) {
922
+ linkStruct(iface, link, flags, internalBroken = false) {
879
923
  const result = {
880
924
  SENDER: link.SENDER,
881
925
  RECEIVER: link.RECEIVER,
882
- FLAGS: link.FLAGS || 0,
926
+ FLAGS: (link.FLAGS || 0) | (internalBroken && this.isInternalLink(link) ? LINK_SENDER_BROKEN : 0),
883
927
  NAME: link.NAME || '',
884
928
  DESCRIPTION: link.DESCRIPTION || '',
885
929
  };
@@ -1065,6 +1109,9 @@ class HmSim {
1065
1109
  * plus what was raised through the scenario API.
1066
1110
  */
1067
1111
  getServiceMessages(iface) {
1112
+ if (this.interfaceOption(iface, 'getServiceMessagesFault')) {
1113
+ throw this.fault('unknownMethod', `${iface}/getServiceMessages`);
1114
+ }
1068
1115
  const messages = [];
1069
1116
  for (const address of Object.keys(this.values[iface])) {
1070
1117
  const description = this.getParamsetDescription(iface, address, 'VALUES');
@@ -1073,13 +1120,16 @@ class HmSim {
1073
1120
  }
1074
1121
  for (const [name, value] of Object.entries(this.values[iface][address].VALUES || {})) {
1075
1122
  const parameter = description[name];
1076
- if (parameter && parameter.FLAGS & SERVICE_FLAG && value) {
1123
+ if (parameter && parameter.FLAGS & SERVICE_FLAG && value && !this.isSuppressed(iface, address, name)) {
1077
1124
  messages.push([address, name, value]);
1078
1125
  }
1079
1126
  }
1080
1127
  }
1081
1128
 
1082
1129
  for (const message of this.serviceMessages[iface] || []) {
1130
+ if (this.isSuppressed(iface, message[0], message[1])) {
1131
+ continue;
1132
+ }
1083
1133
  if (!messages.some((entry) => entry[0] === message[0] && entry[1] === message[1])) {
1084
1134
  messages.push([...message]);
1085
1135
  }
@@ -1115,6 +1165,297 @@ class HmSim {
1115
1165
  this.timer(() => this.addDevices(iface, scripted.devices), scripted.delay);
1116
1166
  }
1117
1167
 
1168
+ const mismatch = this.keyMismatchScript[iface];
1169
+ if (mismatch) {
1170
+ this.timer(() => this.hearKeyMismatchDevice(iface, mismatch), mismatch.delay);
1171
+ }
1172
+
1173
+ return '';
1174
+ }
1175
+
1176
+ /* ------------------------------------------------------------------ key mismatch */
1177
+
1178
+ /**
1179
+ * A device that holds another system's security key comes into reach of the next install mode
1180
+ * (or of an `addDevice` with its serial) - the case of Homematic Manager task 76.
1181
+ *
1182
+ * rfd hears the device but cannot pair it: nothing joins and nobody is told; the serial is only
1183
+ * what `getKeyMismatchDevice` answers. Once the temporary key (`setTempKey`) is the device's
1184
+ * `key`, the next install mode pairs it: `devices` (the description and its channels) join with
1185
+ * `newDevices`, and the script is used up. Without `devices` the device is never paired.
1186
+ *
1187
+ * @param {string} iface
1188
+ * @param {string} serial
1189
+ * @param {object} [script]
1190
+ * @param {string} [script.key=''] the passphrase the device was taught in with
1191
+ * @param {Array} [script.devices=[]] its description and channel descriptions
1192
+ * @param {number} [script.delay=0] milliseconds between the install mode and the device being heard
1193
+ */
1194
+ scriptKeyMismatch(iface, serial, {key = '', devices = [], delay = 0} = {}) {
1195
+ if (typeof serial !== 'string' || serial === '') {
1196
+ throw new TypeError('scriptKeyMismatch wants a serial');
1197
+ }
1198
+ this.keyMismatchScript[iface] = {serial, key: String(key), devices: devices || [], delay};
1199
+ }
1200
+
1201
+ /** The scripted device is heard: paired when the temporary key is its own, else a key mismatch. */
1202
+ hearKeyMismatchDevice(iface, script) {
1203
+ if (this.keyMismatchScript[iface] !== script) {
1204
+ return undefined;
1205
+ }
1206
+ if (script.key !== '' && this.getTempKey(iface) === script.key && script.devices.length > 0) {
1207
+ delete this.keyMismatchScript[iface];
1208
+ this.addDevices(iface, script.devices);
1209
+ this.log.info(iface, script.serial, 'paired with the temporary key');
1210
+ return this.index[iface].get(script.devices[0].ADDRESS) || script.devices[0];
1211
+ }
1212
+ this.keyMismatch[iface] = script.serial;
1213
+ this.log.info(iface, script.serial, 'heard, but it holds another key');
1214
+ return undefined;
1215
+ }
1216
+
1217
+ /**
1218
+ * `getKeyMismatchDevice(reset)` - the serial of the last device that could not be paired
1219
+ * because it holds another key, `''` when there is none (measured on rfd and hmipserver,
1220
+ * firmware 3.89.11). `true` clears it after the answer.
1221
+ */
1222
+ getKeyMismatchDevice(iface, params) {
1223
+ if (iface !== 'rfd' && iface !== 'hmip') {
1224
+ throw this.fault('unknownMethod', `${iface}/getKeyMismatchDevice`);
1225
+ }
1226
+ const [reset = false] = params || [];
1227
+ const serial = this.keyMismatch[iface] || '';
1228
+ if (reset) {
1229
+ delete this.keyMismatch[iface];
1230
+ }
1231
+ return serial;
1232
+ }
1233
+
1234
+ /**
1235
+ * `addDevice(serial)` - rfd pairs one device by its serial. The simulator knows only the device
1236
+ * of scriptKeyMismatch(): with the right temporary key it joins and its description is the
1237
+ * answer; otherwise it is the key mismatch and the call faults. Any other serial is not in reach
1238
+ * (`unknownInstance`). BidCos only.
1239
+ */
1240
+ addDeviceBySerial(iface, params) {
1241
+ if (!BIDCOS_INTERFACES.has(iface)) {
1242
+ throw this.fault('unknownMethod', `${iface}/addDevice`);
1243
+ }
1244
+ const [serial] = params || [];
1245
+ const script = this.keyMismatchScript[iface];
1246
+ if (!script || script.serial !== serial) {
1247
+ throw this.fault('unknownInstance', `${iface}/${serial}`);
1248
+ }
1249
+ const paired = this.hearKeyMismatchDevice(iface, script);
1250
+ if (!paired) {
1251
+ throw this.fault('notSupported', `${serial} holds another key`);
1252
+ }
1253
+ return paired;
1254
+ }
1255
+
1256
+ /* ------------------------------------------------------------------ service message suppression */
1257
+
1258
+ /**
1259
+ * `suppressServiceMessages(channelAddress, parameter, suppress)` - hmipserver only (eQ-3's HmIP
1260
+ * addendum; rfd does not have it). `parameter` is one of the channel's service parameters, or
1261
+ * `''` for all of them; a parameter without the service flag is ignored. Measured on firmware
1262
+ * 3.89.11: answered with an empty value, `''` suppressed every service parameter of the
1263
+ * channel, `LEVEL` nothing. A suppressed message is left out of `getServiceMessages`, and the
1264
+ * datapoint reports the value that raises no message (`false`, `0`) - the addendum's words; that
1265
+ * a change of the suppression sends that value as an event is the simulator's model.
1266
+ */
1267
+ suppressServiceMessages(iface, params) {
1268
+ if (iface !== 'hmip') {
1269
+ throw this.fault('unknownMethod', `${iface}/suppressServiceMessages`);
1270
+ }
1271
+ const [address, parameter = '', suppress = true] = params || [];
1272
+ if (typeof address !== 'string' || typeof parameter !== 'string') {
1273
+ throw this.fault('invalidArguments', 'suppressServiceMessages wants a channel address and a parameter');
1274
+ }
1275
+ const description = this.getParamsetDescription(iface, address, 'VALUES') || {};
1276
+ const names = Object.keys(description).filter(
1277
+ (name) => description[name].FLAGS & SERVICE_FLAG && (parameter === '' || parameter === name),
1278
+ );
1279
+ const table = (this.suppressed[iface] = this.suppressed[iface] || new Map());
1280
+ const set = table.get(address) || new Set();
1281
+ for (const name of names) {
1282
+ const before = this.reportedValue(iface, address, name);
1283
+ if (suppress) {
1284
+ set.add(name);
1285
+ } else {
1286
+ set.delete(name);
1287
+ }
1288
+ table.set(address, set);
1289
+ const after = this.reportedValue(iface, address, name);
1290
+ if (before !== after && description[name].OPERATIONS & 4) {
1291
+ this.event(iface, [address, name, after]);
1292
+ }
1293
+ }
1294
+ if (set.size === 0) {
1295
+ table.delete(address);
1296
+ }
1297
+ return '';
1298
+ }
1299
+
1300
+ /**
1301
+ * `getSuppressedServiceMessages(channelAddress)` - the suppressed parameters of a channel, sorted;
1302
+ * `[]` for an address hmipserver does not know (measured, firmware 3.89.11). hmipserver only.
1303
+ */
1304
+ getSuppressedServiceMessages(iface, params) {
1305
+ if (iface !== 'hmip') {
1306
+ throw this.fault('unknownMethod', `${iface}/getSuppressedServiceMessages`);
1307
+ }
1308
+ const [address] = params || [];
1309
+ const set = this.suppressed[iface] && this.suppressed[iface].get(address);
1310
+ return set ? [...set].sort() : [];
1311
+ }
1312
+
1313
+ isSuppressed(iface, address, name) {
1314
+ const set = this.suppressed[iface] && this.suppressed[iface].get(address);
1315
+ return Boolean(set && set.has(name));
1316
+ }
1317
+
1318
+ /** The stored value, or for a suppressed service message the value that raises no message. */
1319
+ reportedValue(iface, address, name, value) {
1320
+ const stored =
1321
+ value === undefined
1322
+ ? ((this.values[iface] && this.values[iface][address]) || {VALUES: {}}).VALUES[name]
1323
+ : value;
1324
+ if (!this.isSuppressed(iface, address, name)) {
1325
+ return stored;
1326
+ }
1327
+ return typeof stored === 'boolean' || stored === undefined ? false : 0;
1328
+ }
1329
+
1330
+ /* ------------------------------------------------------------------ metadata */
1331
+
1332
+ requireMetadataInterface(iface, method) {
1333
+ if (!METADATA_INTERFACES.has(iface)) {
1334
+ throw this.fault('unknownMethod', `${iface}/${method}`);
1335
+ }
1336
+ }
1337
+
1338
+ /**
1339
+ * `getMetadata(address, key)` - a value stored with `setMetadata` or the `metadata` option. A key
1340
+ * that is not set: hmipserver answers an empty value, rfd the fault `-1 Failure` - also for an
1341
+ * address it does not know (measured, firmware 3.89.11). rfd, hs485d and hmipserver.
1342
+ */
1343
+ getMetadata(iface, params) {
1344
+ this.requireMetadataInterface(iface, 'getMetadata');
1345
+ const [address, key] = params || [];
1346
+ const stored = this.metadata[iface] && this.metadata[iface][address];
1347
+ if (stored && Object.hasOwn(stored, key)) {
1348
+ return stored[key];
1349
+ }
1350
+ if (iface === 'hmip') {
1351
+ return '';
1352
+ }
1353
+ throw this.fault('notSupported', `no metadata ${key} on ${address}`);
1354
+ }
1355
+
1356
+ /**
1357
+ * `setMetadata(address, key, value)` - stores a value. rfd and hs485d know only their own
1358
+ * addresses (`unknownInstance`); hmipserver takes any (the simulator's model).
1359
+ */
1360
+ setMetadata(iface, params) {
1361
+ this.requireMetadataInterface(iface, 'setMetadata');
1362
+ const [address, key, value] = params || [];
1363
+ if (typeof address !== 'string' || typeof key !== 'string' || key === '') {
1364
+ throw this.fault('invalidArguments', 'setMetadata wants an address and a key');
1365
+ }
1366
+ if (iface !== 'hmip') {
1367
+ this.requireDevice(iface, address);
1368
+ }
1369
+ this.metadata[iface] = this.metadata[iface] || {};
1370
+ this.metadata[iface][address] = this.metadata[iface][address] || {};
1371
+ this.metadata[iface][address][key] = value;
1372
+ return '';
1373
+ }
1374
+
1375
+ /**
1376
+ * `getAllMetadata(address)` - every key of an address. rfd and hs485d only (hmipserver does not
1377
+ * list it); nothing stored is the fault `-1 Failure`, as measured on rfd.
1378
+ */
1379
+ getAllMetadata(iface, params) {
1380
+ if (!BIDCOS_INTERFACES.has(iface)) {
1381
+ throw this.fault('unknownMethod', `${iface}/getAllMetadata`);
1382
+ }
1383
+ const [address] = params || [];
1384
+ const stored = this.metadata[iface] && this.metadata[iface][address];
1385
+ if (!stored || Object.keys(stored).length === 0) {
1386
+ throw this.fault('notSupported', `no metadata on ${address}`);
1387
+ }
1388
+ return {...stored};
1389
+ }
1390
+
1391
+ /* ------------------------------------------------------------------ interface information */
1392
+
1393
+ /**
1394
+ * `listReplaceableDevices(address)` - the devices a device can take the place of. rfd: the other
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 `''`.
1400
+ */
1401
+ listReplaceableDevices(iface, params) {
1402
+ const [address] = params || [];
1403
+ if (iface === 'hmip') {
1404
+ return '';
1405
+ }
1406
+ if (!BIDCOS_INTERFACES.has(iface)) {
1407
+ throw this.fault('unknownMethod', `${iface}/listReplaceableDevices`);
1408
+ }
1409
+ const device = this.requireDevice(iface, address);
1410
+ if (device.PARENT) {
1411
+ return [];
1412
+ }
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
+ );
1417
+ }
1418
+
1419
+ /**
1420
+ * `getVersion()` - the version string of the interface process: `interfaces.<iface>.version`,
1421
+ * by default what rfd (`2.6.0`) and hmipserver (the firmware, `3.89.11.20260919`) answered on
1422
+ * firmware 3.89.11. VirtualDevices and CUxD: unknown-method.
1423
+ */
1424
+ getVersion(iface) {
1425
+ const version = this.interfaceOption(iface, 'version');
1426
+ if (version === undefined) {
1427
+ throw this.fault('unknownMethod', `${iface}/getVersion`);
1428
+ }
1429
+ return version;
1430
+ }
1431
+
1432
+ /**
1433
+ * `getLGWStatus()` - rfd on firmware 3.89.11 answers `unknown method name`, measured on two
1434
+ * systems; `interfaces.rfd.lgwStatus` makes it answer that value instead, for a client that
1435
+ * shows it. BidCos only.
1436
+ */
1437
+ getLGWStatus(iface) {
1438
+ const status = BIDCOS_INTERFACES.has(iface) ? this.interfaceOption(iface, 'lgwStatus') : undefined;
1439
+ if (status === undefined) {
1440
+ throw this.fault('unknownMethod', `${iface}/getLGWStatus`);
1441
+ }
1442
+ return status;
1443
+ }
1444
+
1445
+ /**
1446
+ * `setInterfaceClock(utcSeconds, offsetMinutes)` - rfd and hmipserver set the clock they send the
1447
+ * devices. Recorded in `sim.interfaceClocks` (`{iface, utc, offset, ts}`) and answered with an
1448
+ * empty string (the simulator's model; not measured - it changes the system's clock).
1449
+ */
1450
+ setInterfaceClock(iface, params) {
1451
+ if (iface !== 'rfd' && iface !== 'hmip') {
1452
+ throw this.fault('unknownMethod', `${iface}/setInterfaceClock`);
1453
+ }
1454
+ const [utc, offset = 0] = params || [];
1455
+ if (!Number.isFinite(Number(utc))) {
1456
+ throw this.fault('invalidArguments', 'setInterfaceClock wants the UTC seconds');
1457
+ }
1458
+ this.interfaceClocks.push({iface, utc: Number(utc), offset: Number(offset), ts: Date.now()});
1118
1459
  return '';
1119
1460
  }
1120
1461
 
@@ -1365,6 +1706,12 @@ class HmSim {
1365
1706
  if (this.poisonedChannels[iface]) {
1366
1707
  this.poisonedChannels[iface].delete(entry);
1367
1708
  }
1709
+ if (this.suppressed[iface]) {
1710
+ this.suppressed[iface].delete(entry);
1711
+ }
1712
+ if (this.metadata[iface]) {
1713
+ delete this.metadata[iface][entry];
1714
+ }
1368
1715
  }
1369
1716
  if (this.configPending[iface]) {
1370
1717
  this.configPending[iface].delete(address);
@@ -1413,7 +1760,7 @@ class HmSim {
1413
1760
  }
1414
1761
 
1415
1762
  this.deleteDevice(iface, oldAddress);
1416
- return '';
1763
+ return true;
1417
1764
  }
1418
1765
 
1419
1766
  reportValueUsage(iface, params) {
@@ -2111,6 +2458,17 @@ class HmSim {
2111
2458
  logLevel: (iface, params) => this.logLevel(iface, params),
2112
2459
  changeKey: (iface, params) => this.changeKey(iface, params),
2113
2460
  refreshDeployedDeviceFirmwareList: (iface) => this.refreshDeployedDeviceFirmwareList(iface),
2461
+ getKeyMismatchDevice: (iface, params) => this.getKeyMismatchDevice(iface, params),
2462
+ addDevice: (iface, params) => this.addDeviceBySerial(iface, params),
2463
+ suppressServiceMessages: (iface, params) => this.suppressServiceMessages(iface, params),
2464
+ getSuppressedServiceMessages: (iface, params) => this.getSuppressedServiceMessages(iface, params),
2465
+ getMetadata: (iface, params) => this.getMetadata(iface, params),
2466
+ setMetadata: (iface, params) => this.setMetadata(iface, params),
2467
+ getAllMetadata: (iface, params) => this.getAllMetadata(iface, params),
2468
+ listReplaceableDevices: (iface, params) => this.listReplaceableDevices(iface, params),
2469
+ getVersion: (iface) => this.getVersion(iface),
2470
+ getLGWStatus: (iface) => this.getLGWStatus(iface),
2471
+ setInterfaceClock: (iface, params) => this.setInterfaceClock(iface, params),
2114
2472
  };
2115
2473
 
2116
2474
  // the CCU accepts the lower case spelling as well
@@ -2580,7 +2938,11 @@ class HmSim {
2580
2938
  const events =
2581
2939
  parameter.TYPE === 'ACTION'
2582
2940
  ? [[address, datapoint, casted]]
2583
- : Object.keys(state.VALUES).map((name) => [address, name, state.VALUES[name]]);
2941
+ : Object.keys(state.VALUES).map((name) => [
2942
+ address,
2943
+ name,
2944
+ this.reportedValue(iface, address, name, state.VALUES[name]),
2945
+ ]);
2584
2946
  this.eventMulticall(iface, events);
2585
2947
  }
2586
2948
  }
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "hm-simulator",
3
- "version": "1.2.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
+ "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};