hm-simulator 1.1.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/lib/validate.js CHANGED
@@ -98,6 +98,10 @@ function castValue(description, name, value, fault, {write = true} = {}) {
98
98
  /** The value a parameter has before anything was written, taken from its description. */
99
99
  function defaultValue(parameter) {
100
100
  if (parameter.TYPE === 'ENUM' && Array.isArray(parameter.VALUE_LIST)) {
101
+ // the descriptions carry DEFAULT as the name or, as often, already as the index
102
+ if (Number.isInteger(parameter.DEFAULT)) {
103
+ return parameter.DEFAULT;
104
+ }
101
105
  const index = parameter.VALUE_LIST.indexOf(parameter.DEFAULT);
102
106
  return index === -1 ? 0 : index;
103
107
  }
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "hm-simulator",
3
- "version": "1.1.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,10 +46,13 @@
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/",
50
54
  "README.md",
55
+ "CHANGELOG.md",
51
56
  "LICENSE"
52
57
  ],
53
58
  "dependencies": {
@@ -56,9 +61,11 @@
56
61
  },
57
62
  "devDependencies": {
58
63
  "@eslint/js": "^9.39.5",
64
+ "@types/node": "^20.19.43",
59
65
  "eslint": "^9.39.5",
60
66
  "eslint-config-prettier": "^10.1.8",
61
67
  "globals": "^16.5.0",
62
- "prettier": "^3.9.6"
68
+ "prettier": "^3.9.6",
69
+ "typescript": "^5.9.3"
63
70
  }
64
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;