hm-simulator 0.1.1 → 1.0.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/sim.js CHANGED
@@ -1,542 +1,4 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const EventEmitter = require('events');
4
- const express = require('express');
5
- const bodyParser = require('body-parser');
6
- const binrpc = require('binrpc');
7
- const xmlrpc = require('homematic-xmlrpc');
8
-
9
- module.exports = class HmSim {
10
- constructor(options = {}) {
11
- const log = options.log || {
12
- debug: () => {},
13
- info: () => {},
14
- warn: () => {},
15
- error: () => {}
16
- };
17
-
18
- this.log = log;
19
-
20
- const config = options.config || {};
21
-
22
- const devices = options.devices || {rfd: {devices: []}, hmip: {devices: []}};
23
-
24
- const paramsetDescriptions = require('./data/paramset-descriptions.json');
25
-
26
- const clients = {
27
- rfd: {},
28
- hmip: {}
29
- };
30
-
31
- const values = {
32
- rfd: {},
33
- hmip: {},
34
- sysvars: {},
35
- programs: {}
36
- };
37
-
38
- setDefaultValues('rfd');
39
- setDefaultValues('hmip');
40
-
41
- function setDefaultValues(iface) {
42
- devices[iface].devices.forEach(dev => {
43
- if (dev.PARENT_TYPE) {
44
- if (dev.PARAMSETS.includes('VALUES')) {
45
- const ps = getParamsetDescription(iface, dev, 'VALUES');
46
- Object.keys(ps).forEach(dp => {
47
- if (!values[iface][dev.ADDRESS]) {
48
- values[iface][dev.ADDRESS] = {
49
- VALUES: {}
50
- };
51
- }
52
-
53
- if (ps[dp].type === 'ENUM') {
54
- values[iface][dev.ADDRESS].VALUES[dp] = ps[dp].VALUE_LIST.indexOf(ps[dp].DEFAULT);
55
- } else {
56
- values[iface][dev.ADDRESS].VALUES[dp] = ps[dp].DEFAULT;
57
- }
58
- });
59
- }
60
- }
61
- });
62
- }
63
-
64
- const rfdServer = binrpc.createServer({host: config.listenAddress, port: config.binrpcListenPort});
65
- log.info('rfd binrpc server listening on', config.listenAddress, config.binrpcListenPort);
66
- this.rfdServer = rfdServer;
67
-
68
- const hmipServer = xmlrpc.createServer({host: config.listenAddress, port: config.xmlrpcListenPort});
69
- log.info('hmip xmlrpc server listening on', config.listenAddress, config.xmlrpcListenPort);
70
- this.hmipServer = hmipServer;
71
-
72
- const rpcMethods = {
73
- 'system.listMethods': (err, iface, params, callback) => {
74
- callback(null, Object.keys(rpcMethods));
75
- },
76
- init: (err, iface, params, callback) => {
77
- const [url, id] = params;
78
- let [protocol, host, port] = url.split(':');
79
- host = host.replace(/^\/\//, '');
80
- if (protocol === 'xmlrpc_bin') {
81
- protocol = 'binrpc';
82
- }
83
-
84
- const clientId = [host, port].join(':');
85
-
86
- if (id === '') {
87
- log.debug('remove', iface, clients[iface][clientId].url);
88
- let client = clients[iface][clientId];
89
- // Todo ...
90
- if (client && typeof client.client.end === 'function') {
91
- client.client.end();
92
- }
93
-
94
- if (client && typeof client.client.close === 'function') {
95
- client.client.close();
96
- }
97
-
98
- if (client && typeof client.client.destroy === 'function') {
99
- client.client.destroy();
100
- }
101
-
102
- client = null;
103
- delete clients[iface][clientId];
104
- } else {
105
- clients[iface][clientId] = {
106
- id,
107
- url,
108
- client: (protocol === 'binrpc' ? binrpc : xmlrpc).createClient({host, port}),
109
- methodCall: (methodName, params, callback) => {
110
- log.debug('rpc >', url, methodName, shortenParams(params));
111
- clients[iface][clientId].client.methodCall(methodName, params, (err, res) => {
112
- log.debug('rpc <', url, shortenParams(res));
113
- if (typeof callback === 'function') {
114
- callback(err, res);
115
- }
116
- });
117
- }
118
- };
119
- startInit(iface, clients[iface][clientId]);
120
- }
121
-
122
- callback(null, '');
123
- },
124
- listDevices: (err, iface, params, callback) => {
125
- callback(null, devices[iface].devices);
126
- },
127
- getParamsetDescription: (err, iface, params, callback) => {
128
- const [address, paramset] = params;
129
- callback(null, getParamsetDescription(iface, address, paramset));
130
- },
131
- ping: (err, iface, params, callback) => {
132
- if (iface === 'hmip') {
133
- // Todo Remove when https://github.com/eq-3/occu/issues/42 is fixed
134
- return;
135
- }
136
-
137
- const [id] = params;
138
- event(iface, ['CENTRAL', 'PONG', id]);
139
- callback(null, '');
140
- },
141
- setValue: (err, iface, params, callback) => {
142
- const [address, datapoint, value] = params;
143
- setValue(iface, address, datapoint, value);
144
- callback(null, '');
145
- },
146
- notFound: (methodName, iface, params) => {
147
- log.error('rpc', iface, '< unknown method', methodName, shortenParams(params));
148
- }
149
- };
150
-
151
- rpcMethods.NotFound = rpcMethods.notFound;
152
-
153
- function event(iface, params) {
154
- Object.keys(clients[iface]).forEach(c => {
155
- const client = clients[iface][c];
156
- params.unshift(client.id);
157
- client.methodCall('event', params);
158
- });
159
- }
160
-
161
- function eventMulticall(iface, events) {
162
- Object.keys(clients[iface]).forEach(c => {
163
- const multicall = [];
164
- const client = clients[iface][c];
165
- events.forEach(singleEvent => {
166
- const ev = [client.id, singleEvent[0], singleEvent[1], singleEvent[2]];
167
- multicall.push({methodName: 'event', params: ev});
168
- });
169
- client.methodCall('system.multicall', [multicall]);
170
- });
171
- }
172
-
173
- function startInit(iface, client) {
174
- client.methodCall('listDevices', [client.id], (err, clientDevices) => {
175
- if (err) {
176
- log.error(err.toString());
177
- } else {
178
- checkDevices(iface, client, clientDevices);
179
- }
180
- });
181
- }
182
-
183
- function checkDevices(iface, client, clientDevices) {
184
- clientDevices = clientDevices || [];
185
- log.info(iface, 'client', client.url, 'knows', clientDevices.length, 'devices');
186
-
187
- const clientDeviceAddresses = [];
188
- clientDevices.forEach(clientDev => {
189
- clientDeviceAddresses.push(clientDev.ADDRESS);
190
- });
191
- const deviceAddresses = [];
192
- devices[iface].devices.forEach(dev => {
193
- deviceAddresses.push(dev.ADDRESS);
194
- });
195
- const newDevices = [];
196
- const deleteDevices = [];
197
-
198
- devices[iface].devices.forEach(dev => {
199
- const clientDeviceIndex = clientDeviceAddresses.indexOf(dev.ADDRESS);
200
- if (clientDeviceIndex === -1) {
201
- log.debug('device unknown by client', dev.ADDRESS);
202
- newDevices.push(dev);
203
- } else {
204
- const clientDev = clientDevices[clientDeviceIndex];
205
- if (iface === 'hmip') {
206
- if (
207
- // Todo remove next line when https://github.com/eq-3/occu/issues/45 is fixed
208
- true ||
209
- // Todo is this correct? https://github.com/eq-3/occu/issues/43
210
- clientDev.VERSION !== dev.VERSION ||
211
- clientDev.AES_ACTIVE !== dev.AES_ACTIVE ||
212
- clientDev.CHILDREN !== dev.CHILDREN ||
213
- clientDev.DIRECTION !== dev.DIRECTION ||
214
- clientDev.FIRMWARE !== dev.FIRMWARE ||
215
- clientDev.FLAGS !== dev.FLAGS ||
216
- clientDev.GROUP !== dev.GROUP ||
217
- clientDev.INDEX !== dev.INDEX ||
218
- clientDev.INTERFACE !== dev.INTERFACE ||
219
- clientDev.LINK_SOURCE_ROLES !== dev.LINK_SOURCE_ROLES ||
220
- clientDev.LINK_TARGET_ROLES !== dev.LINK_TARGET_ROLES ||
221
- clientDev.PARAMSETS !== dev.PARAMSETS ||
222
- clientDev.PARENT !== dev.PARENT ||
223
- clientDev.PARENT_TYPE !== dev.PARENT_TYPE ||
224
- clientDev.RF_ADDRESS !== dev.RF_ADDRESS ||
225
- clientDev.ROAMING !== dev.ROAMING ||
226
- clientDev.RX_MODE !== dev.RX_MODE ||
227
- clientDev.TEAM !== dev.TEAM ||
228
- clientDev.TEAM_CHANNELS !== dev.TEAM_CHANNELS ||
229
- clientDev.TEAM_TAG !== dev.TEAM_TAG ||
230
- clientDev.TYPE !== dev.TYPE
231
- ) {
232
- // Todo log.debug('device mismatch', clientDev.ADDRESS);
233
- deleteDevices.push(clientDev.ADDRESS);
234
- newDevices.push(clientDev);
235
- }
236
- } else if (clientDev.VERSION !== dev.VERSION) {
237
- log.debug('device mismatch', clientDev.ADDRESS);
238
- deleteDevices.push(clientDev.ADDRESS);
239
- newDevices.push(clientDev);
240
- }
241
- }
242
- });
243
- clientDevices.forEach(clientDev => {
244
- if (!deviceAddresses.includes(clientDev.ADDRESS)) {
245
- log.debug('device unknown', clientDev.ADDRESS);
246
- deleteDevices.push(clientDev.ADDRESS);
247
- }
248
- });
249
- if (deleteDevices.length > 0) {
250
- log.info(iface, 'client', client.url, 'should delete', deleteDevices.length, 'devices');
251
- client.methodCall('deleteDevices', [client.id, deleteDevices], () => {
252
- if (newDevices.length > 0) {
253
- log.info(iface, 'client', client.url, 'should add', newDevices.length, 'devices');
254
- client.methodCall('newDevices', [client.id, newDevices]);
255
- }
256
- });
257
- } else if (newDevices.length > 0) {
258
- log.info(iface, 'client', client.url, 'should add', newDevices.length, 'devices');
259
- client.methodCall('newDevices', [client.id, newDevices]);
260
- } else {
261
- log.info(iface, 'client', client.url, 'all devices known');
262
- }
263
- }
264
-
265
- rpcMethods['system.listmethods'] = (err, iface, params, callback) => {
266
- callback(null, Object.keys(rpcMethods));
267
- };
268
-
269
- Object.keys(rpcMethods).forEach(methodName => {
270
- rfdServer.on(methodName, (err, params, callback) => {
271
- log.debug('rpc rfd <', methodName, shortenParams(params));
272
- rpcMethods[methodName](err, 'rfd', params, callback);
273
- });
274
- hmipServer.on(methodName, (err, params, callback) => {
275
- log.debug('rpc hmip <', methodName, shortenParams(params));
276
- rpcMethods[methodName](err, 'hmip', params, callback);
277
- });
278
- });
279
-
280
- function shortenParams(params) {
281
- if (!params) {
282
- return;
283
- }
284
-
285
- const str = JSON.stringify(params);
286
- if (str.length > 77) {
287
- return str.slice(0, 77) + '...';
288
- }
289
-
290
- return str;
291
- }
292
-
293
- function getDevice(iface, address) {
294
- const devs = devices[iface].devices;
295
- for (const element of devs) {
296
- if (element.ADDRESS === address) {
297
- return element;
298
- }
299
- }
300
-
301
- log.error(iface, 'unknown device', address);
302
- return false;
303
- }
304
-
305
- function paramsetName(iface, device, paramset) {
306
- let cType = '';
307
- let d;
308
- if (device) {
309
- if (device.PARENT) {
310
- // Channel
311
- cType = device.TYPE;
312
- const devs = devices[iface].devices;
313
- for (const element of devs) {
314
- if (element.ADDRESS === device.PARENT) {
315
- d = element;
316
- break;
317
- }
318
- }
319
- } else {
320
- // Device
321
- d = device;
322
- }
323
-
324
- switch (iface) {
325
- case 'rfd':
326
- iface = 'BidCos-RF';
327
- break;
328
- case 'hmip':
329
- iface = 'HmIP-RF';
330
- break;
331
- default:
332
- }
333
-
334
- return [iface, d.TYPE, d.FIRMWARE, d.VERSION, cType, paramset].join('/');
335
- }
336
- }
337
-
338
- function getParamsetDescription(iface, dev, paramset) {
339
- if (typeof dev === 'string') {
340
- const devs = devices[iface].devices;
341
- for (const element of devs) {
342
- if (element.ADDRESS === dev) {
343
- dev = element;
344
- break;
345
- }
346
- }
347
- }
348
-
349
- const psName = paramsetName(iface, dev, paramset);
350
- return paramsetDescriptions[psName];
351
- }
352
-
353
- function setValue(iface, address, datapoint, value) {
354
- log.debug('setValue', iface, address, datapoint, value);
355
- const dev = getDevice(iface, address);
356
- if (dev) {
357
- const ps = getParamsetDescription(iface, address, 'VALUES');
358
- if (ps && ps[datapoint]) {
359
- switch (ps[datapoint].TYPE) {
360
- case 'ACTION':
361
- case 'BOOL':
362
- if (typeof value !== 'boolean') {
363
- log.error('type mismatch', address, datapoint, ps[datapoint].TYPE);
364
- return;
365
- }
366
-
367
- break;
368
- case 'INTEGER':
369
- case 'FLOAT':
370
- if (typeof value !== 'number') {
371
- log.error('type mismatch', address, datapoint, ps[datapoint].TYPE);
372
- return;
373
- }
374
-
375
- if ((value < ps[datapoint].MIN) || (value > ps[datapoint].MAx)) {
376
- log.error('range error', address, datapoint, ps[datapoint].MIN, ps[datapoint].MAX);
377
- return;
378
- }
379
-
380
- break;
381
- default:
382
- }
383
-
384
- values[iface][address].VALUES[datapoint] = value;
385
-
386
- if (ps[datapoint].OPERATIONS & 4) {
387
- const events = [];
388
- if (ps[datapoint].TYPE === 'ACTION') {
389
- events.push([address, datapoint, values[iface][address].VALUES[datapoint]]);
390
- } else {
391
- Object.keys(values[iface][address].VALUES).forEach(dp => {
392
- events.push([address, dp, values[iface][address].VALUES[dp]]);
393
- });
394
- }
395
-
396
- eventMulticall(iface, events);
397
- }
398
- } else {
399
- log.error('unknown params', address, datapoint);
400
- }
401
- }
402
- }
403
-
404
- this.api = new EventEmitter();
405
-
406
- this.api.on('setValue', setValue);
407
-
408
- this.loadBehaviors(options.behaviorPath || path.join(__dirname, 'behaviors'));
409
-
410
- if (options.rega) {
411
- this.regaSim = new RegaSim(options.rega, this.log);
412
- }
413
- }
414
-
415
- loadBehaviors(p) {
416
- const files = fs.readdirSync(p);
417
- files.forEach(file => {
418
- if (file.match(/\.js$/)) {
419
- this.log.info('loading behavior', file);
420
- require(path.join(p, file))(this.api);
421
- }
422
- });
423
- }
424
-
425
- close() {
426
- this.api.removeAllListeners();
427
- this.rfdServer.server.close();
428
- this.hmipServer.httpServer.close();
429
- if (this.regaSim) {
430
- this.regaSim.close();
431
- }
432
- }
433
- };
434
-
435
- class RegaSim {
436
- constructor(options, log) {
437
- this.variables = options.variables || [];
438
- this.devices = options.channels || [];
439
- this.programs = options.programs || [];
440
- this.rooms = options.rooms || [];
441
- this.functions = options.functions || [];
442
-
443
- this.app = express();
444
- this.app.use(bodyParser.raw({type: 'application/*'}));
445
- this.app.get('/[a-zA-Z_-]+.exe', (req, res) => {
446
- console.log('get', req.url, req.queryString);
447
- res.send(this.response(''));
448
- });
449
-
450
- this.app.post('/[a-zA-Z_-]+.exe', (req, res) => {
451
- const script = req.body.toString();
452
- const line = script.split('\n')[0];
453
- let response;
454
- switch (line) {
455
- case '!# devices.rega':
456
- response = this.response(this.devices);
457
- break;
458
- case '!# variables.rega':
459
- response = this.response(this.variables);
460
- break;
461
-
462
- case '!# programs.rega':
463
- response = this.response(this.programs);
464
- break;
465
- case '!# rooms.rega':
466
- response = this.response(this.rooms);
467
- break;
468
- case '!# functions.rega':
469
- response = this.response(this.functions);
470
- break;
471
-
472
- default:
473
- let match;
474
- if (match = line.match(/dom\.GetObject\((\d+)\).State\(([^)]*)\)/)) {
475
- let [, id, val] = match;
476
- id = parseInt(id, 10);
477
- val = JSON.parse(val || '');
478
- this.variables.forEach((v, i) => {
479
- if (v.id === id) {
480
- this.variables[i].val = val;
481
- this.variables[i].ts = this.ts();
482
- }
483
- });
484
- } else {
485
- log.warn('unknown script', line);
486
- }
487
-
488
- response = this.response('');
489
- }
490
-
491
- res.set({
492
- server: 'ise GmbH HTTP-Server v2.0',
493
- 'accept-ranges': 'bytes',
494
- 'cache-control': 'no-store, no-cache',
495
- 'content-type': 'text/xml; charset=iso-8859-1',
496
- 'content-length': response.length
497
- });
498
- res.send(response);
499
- });
500
-
501
- this.server = this.app.listen(options.port, () => {
502
- // Console.log('RegaSim listening on Port ' + options.port);
503
- });
504
- }
505
-
506
- close() {
507
- this.server.close();
508
- }
509
-
510
- response(stdout, vars = []) {
511
- if (typeof stdout !== 'string') {
512
- stdout = JSON.stringify(stdout);
513
- }
514
-
515
- const xml = [
516
- ['exec', 'rega.exe'],
517
- ['sessionId', ''],
518
- ['httpUserAgent', '']
519
- ].concat(vars);
520
-
521
- stdout += '<xml>';
522
-
523
- xml.forEach(kv => {
524
- const [key, value] = kv;
525
- stdout += `<${key}>${value}</${key}>`;
526
- });
527
- stdout += '</xml>';
528
-
529
- return stdout;
530
- }
531
-
532
- ts() {
533
- const d = new Date();
534
- return d.getFullYear() + '-' +
535
- ('0' + (d.getMonth() + 1)).slice(-2) + '-' +
536
- ('0' + d.getDate()).slice(-2) + ' ' +
537
- ('0' + d.getHours()).slice(-2) + ':' +
538
- ('0' + d.getMinutes()).slice(-2) + ':' +
539
- ('0' + d.getSeconds()).slice(-2);
540
- }
541
- }
1
+ 'use strict';
542
2
 
3
+ // kept at the package root because consumers require(hm-simulator/sim) and require(hm-simulator/sim.js)
4
+ module.exports = require('./lib/sim.js');
package/sim.mjs ADDED
@@ -0,0 +1,5 @@
1
+ // ESM wrapper. The implementation stays CommonJS because the consumers require() it.
2
+ import HmSim from './sim.js';
3
+
4
+ export default HmSim;
5
+ export {HmSim};
package/.travis.yml DELETED
@@ -1,3 +0,0 @@
1
- language: node_js
2
- node_js:
3
- - '6'
package/config.js DELETED
@@ -1,3 +0,0 @@
1
- module.exports = {
2
-
3
- };