hm-simulator 1.4.0 → 1.5.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 +28 -7
- package/README.md +62 -4
- package/data/fixtures/hmip-wired.json +25645 -0
- package/data/paramset-ids.json +1 -0
- package/index.js +5 -4
- package/lib/sim.js +120 -3
- package/package.json +1 -1
- package/sim.d.ts +11 -0
package/lib/sim.js
CHANGED
|
@@ -48,6 +48,32 @@ const BIDCOS_INTERFACES = new Set(['rfd', 'wired']);
|
|
|
48
48
|
/** FLAGS bit of a link whose sender rfd counts as broken (bit 2 is the receiver) */
|
|
49
49
|
const LINK_SENDER_BROKEN = 1;
|
|
50
50
|
|
|
51
|
+
/** data/paramset-ids.json, read on the first getParamsetId of a BidCos interface */
|
|
52
|
+
let bundledParamsetIds;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The paramset ids of a BidCos device type and version from the device files
|
|
56
|
+
* (`tools/paramset-ids.js`); a version the files lack takes the highest one below it, else the
|
|
57
|
+
* lowest. `undefined` for a type they do not describe.
|
|
58
|
+
*/
|
|
59
|
+
function bidcosParamsetIds(prefix, type, version) {
|
|
60
|
+
if (!bundledParamsetIds) {
|
|
61
|
+
bundledParamsetIds = require('../data/paramset-ids.json');
|
|
62
|
+
}
|
|
63
|
+
const versions = (bundledParamsetIds[prefix] || {})[type];
|
|
64
|
+
if (!versions) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
if (versions[String(version)]) {
|
|
68
|
+
return versions[String(version)];
|
|
69
|
+
}
|
|
70
|
+
const known = Object.keys(versions)
|
|
71
|
+
.map(Number)
|
|
72
|
+
.sort((a, b) => a - b);
|
|
73
|
+
const below = known.filter((entry) => entry <= Number(version));
|
|
74
|
+
return versions[String(below.length > 0 ? below.at(-1) : known[0])];
|
|
75
|
+
}
|
|
76
|
+
|
|
51
77
|
/** The device part of a channel address: `ABC0000001:1` -> `ABC0000001`. */
|
|
52
78
|
/** An SGTIN or an HmIP device key as the comparisons take it: no dashes or blanks, upper case. */
|
|
53
79
|
function normaliseHex(text) {
|
|
@@ -73,6 +99,9 @@ const STANDARD_PARAMSETS = new Set(['MASTER', 'VALUES', 'SERVICE']);
|
|
|
73
99
|
/** FLAGS bit of a datapoint that is a service message (1 visible, 2 internal, 4 transform, 8 service) */
|
|
74
100
|
const SERVICE_FLAG = 8;
|
|
75
101
|
|
|
102
|
+
/** FLAGS bit of a datapoint that keeps its value until a client acknowledges it (STICKY_UNREACH) */
|
|
103
|
+
const STICKY_FLAG = 16;
|
|
104
|
+
|
|
76
105
|
/**
|
|
77
106
|
* Per interface behaviour, overridable with the `interfaces` option.
|
|
78
107
|
*
|
|
@@ -103,6 +132,10 @@ const SERVICE_FLAG = 8;
|
|
|
103
132
|
* unreachWrites
|
|
104
133
|
* what a setValue/putParamset to an unreachable device answers: 'accept' (the default) or
|
|
105
134
|
* 'fault' (notReachable). The simulator's model; not measured yet.
|
|
135
|
+
* stickyWriteEvents
|
|
136
|
+
* whether a client's write to a sticky datapoint (FLAGS & 16: the acknowledgement of
|
|
137
|
+
* STICKY_UNREACH) is answered with the channel's events like every other write (true, the
|
|
138
|
+
* default), or with none (false): a registered client on rfd 3.89.11 got no event for it.
|
|
106
139
|
* firmwareUpdateDelay
|
|
107
140
|
* milliseconds per step of a firmware update, see offerFirmware().
|
|
108
141
|
* protocols
|
|
@@ -122,6 +155,7 @@ const INTERFACE_DEFAULTS = {
|
|
|
122
155
|
deliveryTimeout: 10000,
|
|
123
156
|
// device health, see setReachable() and offerFirmware()
|
|
124
157
|
unreachWrites: 'accept',
|
|
158
|
+
stickyWriteEvents: true,
|
|
125
159
|
firmwareUpdateDelay: 0,
|
|
126
160
|
configPendingMode: 'strict',
|
|
127
161
|
configPendingOnWrite: false,
|
|
@@ -217,6 +251,10 @@ function shortenParams(params) {
|
|
|
217
251
|
* @param {boolean} [options.paramsetFallback=true] a device whose firmware has no description uses
|
|
218
252
|
* the nearest firmware of the same type and VERSION; `false` for exact keys only
|
|
219
253
|
* @param {object} [options.faults] overrides for the fault table, see lib/faults.js
|
|
254
|
+
* @param {object} [options.paramsets] stored values to start with,
|
|
255
|
+
* `{<iface>: {<address>: {<MASTER|VALUES|SERVICE|peer address>: {}}}}`
|
|
256
|
+
* @param {object} [options.paramsetIds] what `getParamsetId` answers before the bundled ids,
|
|
257
|
+
* `{<iface>: {<address>: {<paramset>: id}}}`
|
|
220
258
|
*/
|
|
221
259
|
class HmSim {
|
|
222
260
|
constructor(options = {}) {
|
|
@@ -233,6 +271,10 @@ class HmSim {
|
|
|
233
271
|
/** requested key -> the key used for it (a fallback) or null, see resolveParamsetKey() */
|
|
234
272
|
this.paramsetResolved = new Map();
|
|
235
273
|
this.paramsetIndex = new ParamsetIndex(this.paramsetDescriptions);
|
|
274
|
+
/** stored values a paramset starts with, on top of its defaults: {<iface>: {<address>: {<key>: {}}}} */
|
|
275
|
+
this.seededParamsets = structuredClone(options.paramsets || {});
|
|
276
|
+
/** getParamsetId answers given in the options: {<iface>: {<address>: {<paramset>: id}}} */
|
|
277
|
+
this.paramsetIdOverrides = options.paramsetIds || {};
|
|
236
278
|
|
|
237
279
|
const {table, fault} = createFaults(options.faults);
|
|
238
280
|
this.faults = table;
|
|
@@ -375,6 +417,7 @@ class HmSim {
|
|
|
375
417
|
this.newDevicesScript[iface] = {delay: 0, ...this.options.newDevices[iface]};
|
|
376
418
|
}
|
|
377
419
|
this.reindex(iface);
|
|
420
|
+
this.seedValues(iface);
|
|
378
421
|
this.setDefaultValues(iface);
|
|
379
422
|
if (!this.ifaces.includes(iface)) {
|
|
380
423
|
this.ifaces.push(iface);
|
|
@@ -389,6 +432,22 @@ class HmSim {
|
|
|
389
432
|
this.index[iface] = index;
|
|
390
433
|
}
|
|
391
434
|
|
|
435
|
+
/** The VALUES of `options.paramsets`, before setDefaultValues fills in the rest. */
|
|
436
|
+
seedValues(iface) {
|
|
437
|
+
for (const [address, paramsets] of Object.entries(this.seededParamsets[iface] || {})) {
|
|
438
|
+
if (paramsets.VALUES && this.index[iface].has(address)) {
|
|
439
|
+
const state = this.stateOf(iface, address);
|
|
440
|
+
state.VALUES = {...state.VALUES, ...paramsets.VALUES};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** What `options.paramsets` gives a paramset to start with, or `{}`. */
|
|
446
|
+
seededParamset(iface, address, key) {
|
|
447
|
+
const paramsets = this.seededParamsets[iface] && this.seededParamsets[iface][address];
|
|
448
|
+
return (paramsets && paramsets[key]) || {};
|
|
449
|
+
}
|
|
450
|
+
|
|
392
451
|
setDefaultValues(iface) {
|
|
393
452
|
for (const device of this.devices[iface].devices) {
|
|
394
453
|
if (!device.PARENT_TYPE || !device.PARAMSETS || !device.PARAMSETS.includes('VALUES')) {
|
|
@@ -568,14 +627,17 @@ class HmSim {
|
|
|
568
627
|
const description = this.requireParamsetDescription(iface, device, 'LINK');
|
|
569
628
|
state.LINKS = state.LINKS || {};
|
|
570
629
|
if (!state.LINKS[paramset]) {
|
|
571
|
-
state.LINKS[paramset] =
|
|
630
|
+
state.LINKS[paramset] = {
|
|
631
|
+
...defaultParamset(description),
|
|
632
|
+
...this.seededParamset(iface, address, paramset),
|
|
633
|
+
};
|
|
572
634
|
}
|
|
573
635
|
return {device, description, values: state.LINKS[paramset]};
|
|
574
636
|
}
|
|
575
637
|
|
|
576
638
|
const description = this.requireParamsetDescription(iface, device, paramset);
|
|
577
639
|
if (!state[paramset]) {
|
|
578
|
-
state[paramset] = defaultParamset(description);
|
|
640
|
+
state[paramset] = {...defaultParamset(description), ...this.seededParamset(iface, address, paramset)};
|
|
579
641
|
}
|
|
580
642
|
return {device, description, values: state[paramset]};
|
|
581
643
|
}
|
|
@@ -606,6 +668,56 @@ class HmSim {
|
|
|
606
668
|
return this.getParamsetDescription(iface, device, paramset);
|
|
607
669
|
}
|
|
608
670
|
|
|
671
|
+
/**
|
|
672
|
+
* The RPC `getParamsetId(address, paramset)`: the CCU's own name of a paramset, by which its WebUI
|
|
673
|
+
* picks a form (`easymodes/<id>.tcl`). Measured on firmware 3.89.11:
|
|
674
|
+
*
|
|
675
|
+
* - hmipserver builds it: the device type, the channel index and the key, lower case and joined
|
|
676
|
+
* by `_` - `hmipw-drbl4_2_link`, `hmipw-drbl4_master` for the device - for every key it is
|
|
677
|
+
* given, a peer address too (`hmipw-dri16_1_00000000000102:1`), `null` for none.
|
|
678
|
+
* - rfd and hs485d answer the `id` the device file gives the paramset (`cc_tc_ch_wsr_link`,
|
|
679
|
+
* `hmw_input_ch_master`; `data/paramset-ids.json`): MASTER for a device address and MASTER,
|
|
680
|
+
* VALUES, LINK for a channel; any other key on a channel is taken as a peer address and answers
|
|
681
|
+
* the LINK id. A paramset the address does not have is `-3 Unknown paramset`
|
|
682
|
+
* (`unknownParamsetDescription`). A device type the bundled ids lack answers `''`.
|
|
683
|
+
*
|
|
684
|
+
* An unknown address is `unknownInstance` on all of them. `options.paramsetIds` answers first.
|
|
685
|
+
* @param {string} iface
|
|
686
|
+
* @param {Array} params [address, paramset]
|
|
687
|
+
*/
|
|
688
|
+
getParamsetId(iface, params) {
|
|
689
|
+
const [address, key] = params || [];
|
|
690
|
+
const device = this.requireDevice(iface, address);
|
|
691
|
+
const overrides = this.paramsetIdOverrides[iface] && this.paramsetIdOverrides[iface][address];
|
|
692
|
+
if (overrides && typeof key === 'string' && overrides[key] !== undefined) {
|
|
693
|
+
return overrides[key];
|
|
694
|
+
}
|
|
695
|
+
const parent = (device.PARENT ? this.index[iface].get(device.PARENT) : device) || {};
|
|
696
|
+
|
|
697
|
+
if (!BIDCOS_INTERFACES.has(iface)) {
|
|
698
|
+
const name = key === undefined || key === null ? 'null' : String(key).toLowerCase();
|
|
699
|
+
const type = String(parent.TYPE || '').toLowerCase();
|
|
700
|
+
return device.PARENT ? `${type}_${device.INDEX}_${name}` : `${type}_${name}`;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const name = !device.PARENT || ['MASTER', 'VALUES', 'LINK'].includes(key) ? key : 'LINK';
|
|
704
|
+
const has = Array.isArray(device.PARAMSETS) ? device.PARAMSETS.includes(name) : name === 'MASTER';
|
|
705
|
+
if (!has || (!device.PARENT && name !== 'MASTER')) {
|
|
706
|
+
throw this.fault('unknownParamsetDescription', `${address}/${key}`);
|
|
707
|
+
}
|
|
708
|
+
const ids = bidcosParamsetIds(PARAMSET_PREFIX[iface], parent.TYPE, parent.VERSION);
|
|
709
|
+
if (!ids) {
|
|
710
|
+
return '';
|
|
711
|
+
}
|
|
712
|
+
if (!device.PARENT) {
|
|
713
|
+
return ids.device.MASTER || '';
|
|
714
|
+
}
|
|
715
|
+
const channel = ids.channels.find(
|
|
716
|
+
(entry) => device.INDEX >= entry.index && device.INDEX < entry.index + entry.count,
|
|
717
|
+
);
|
|
718
|
+
return (channel && channel[name]) || '';
|
|
719
|
+
}
|
|
720
|
+
|
|
609
721
|
getValue(iface, address, datapoint) {
|
|
610
722
|
const {description, values} = this.storedParamset(iface, address, 'VALUES');
|
|
611
723
|
if (!description[datapoint]) {
|
|
@@ -2606,6 +2718,7 @@ class HmSim {
|
|
|
2606
2718
|
},
|
|
2607
2719
|
getValue: (iface, params) => this.getValue(iface, params[0], params[1]),
|
|
2608
2720
|
getParamset: (iface, params) => this.getParamset(iface, params[0], params[1]),
|
|
2721
|
+
getParamsetId: (iface, params) => this.getParamsetId(iface, params),
|
|
2609
2722
|
putParamset: (iface, params) => {
|
|
2610
2723
|
this.requireReachable(iface, params[0]);
|
|
2611
2724
|
return this.putParamset(iface, params[0], params[1], params[2]);
|
|
@@ -3115,7 +3228,11 @@ class HmSim {
|
|
|
3115
3228
|
const state = this.stateOf(iface, address);
|
|
3116
3229
|
state.VALUES[datapoint] = casted;
|
|
3117
3230
|
|
|
3118
|
-
|
|
3231
|
+
// a client's acknowledgement of a sticky datapoint: the value goes, and with
|
|
3232
|
+
// stickyWriteEvents false nobody is told (a strict write is one that came over RPC)
|
|
3233
|
+
const silent = strict && parameter.FLAGS & STICKY_FLAG && !this.interfaceOption(iface, 'stickyWriteEvents');
|
|
3234
|
+
|
|
3235
|
+
if (parameter.OPERATIONS & 4 && !silent) {
|
|
3119
3236
|
const events =
|
|
3120
3237
|
parameter.TYPE === 'ACTION'
|
|
3121
3238
|
? [[address, datapoint, casted]]
|
package/package.json
CHANGED
package/sim.d.ts
CHANGED
|
@@ -142,6 +142,8 @@ declare namespace HmSim {
|
|
|
142
142
|
pingDelay?: number;
|
|
143
143
|
startDelay?: number;
|
|
144
144
|
unreachWrites?: 'accept' | 'fault';
|
|
145
|
+
/** `false`: a client's write to a sticky datapoint (FLAGS & 16) sends no event, as observed on rfd */
|
|
146
|
+
stickyWriteEvents?: boolean;
|
|
145
147
|
firmwareUpdateDelay?: number;
|
|
146
148
|
/** what `getVersion` answers */
|
|
147
149
|
version?: string;
|
|
@@ -183,6 +185,13 @@ declare namespace HmSim {
|
|
|
183
185
|
defaultRssi?: number;
|
|
184
186
|
/** `{<iface>: {<address>: {<key>: value}}}`, what `getMetadata` answers before any `setMetadata` */
|
|
185
187
|
metadata?: Record<string, Record<string, Record<string, unknown>>>;
|
|
188
|
+
/**
|
|
189
|
+
* `{<iface>: {<address>: {<MASTER|VALUES|SERVICE|peer address>: {<parameter>: value}}}}`: stored
|
|
190
|
+
* values a paramset starts with, on top of its description's defaults
|
|
191
|
+
*/
|
|
192
|
+
paramsets?: Record<string, Record<string, Record<string, Record<string, unknown>>>>;
|
|
193
|
+
/** `{<iface>: {<address>: {<paramset>: id}}}`, what `getParamsetId` answers before the bundled ids */
|
|
194
|
+
paramsetIds?: Record<string, Record<string, Record<string, string>>>;
|
|
186
195
|
tls?: TlsOption;
|
|
187
196
|
auth?: BasicAuth;
|
|
188
197
|
}
|
|
@@ -500,6 +509,8 @@ declare class HmSim {
|
|
|
500
509
|
): void;
|
|
501
510
|
getLinks(iface: string, params?: unknown[]): Array<HmSim.Link & Record<string, unknown>>;
|
|
502
511
|
getLinkPeers(iface: string, params: unknown[]): string[];
|
|
512
|
+
/** `getParamsetId(address, paramset)`: hmipserver's built id, or the BidCos device file's */
|
|
513
|
+
getParamsetId(iface: string, params: [address: string, paramset?: string]): string;
|
|
503
514
|
deleteDevice(iface: string, address: string, flags?: number): '';
|
|
504
515
|
replaceDevice(iface: string, oldAddress: string, newAddress: string): true;
|
|
505
516
|
getKeyMismatchDevice(iface: string, params?: [reset?: boolean]): string;
|