hm-simulator 0.1.0 → 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/index.js CHANGED
@@ -1,20 +1,84 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ 'use strict';
4
+
5
+ const {parseArgs} = require('node:util');
6
+
3
7
  const HmSim = require('./sim.js');
8
+ const pkg = require('./package.json');
9
+
10
+ const {values: args} = parseArgs({
11
+ options: {
12
+ 'listen-address': {type: 'string', default: '127.0.0.1'},
13
+ 'binrpc-port': {type: 'string', default: '2001'},
14
+ 'xmlrpc-port': {type: 'string', default: '2010'},
15
+ 'virtual-port': {type: 'string'},
16
+ 'cuxd-port': {type: 'string'},
17
+ 'rega-port': {type: 'string', default: '8181'},
18
+ 'no-rega': {type: 'boolean', default: false},
19
+ verbosity: {type: 'string', short: 'v', default: 'info'},
20
+ version: {type: 'boolean', default: false},
21
+ help: {type: 'boolean', short: 'h', default: false},
22
+ },
23
+ });
24
+
25
+ if (args.version) {
26
+ console.log(pkg.version);
27
+ process.exit(0);
28
+ }
4
29
 
5
- const log = require('yalm');
30
+ if (args.help) {
31
+ console.log(`hm-simulator ${pkg.version} - simulates a Homematic CCU
6
32
 
7
- log.setLevel('debug');
33
+ Usage: hm-simulator [options]
34
+
35
+ --listen-address <ip> default 127.0.0.1
36
+ --binrpc-port <port> rfd, default 2001
37
+ --xmlrpc-port <port> hmipserver, default 2010
38
+ --virtual-port <port> VirtualDevices, off unless given
39
+ --cuxd-port <port> CUxD, off unless given
40
+ --rega-port <port> ReGa mock, default 8181
41
+ --no-rega do not start the ReGa mock
42
+ -v, --verbosity <level> error|warn|info|debug, default info
43
+ --version
44
+ -h, --help`);
45
+ process.exit(0);
46
+ }
47
+
48
+ const LEVELS = ['error', 'warn', 'info', 'debug'];
49
+ const level = LEVELS.indexOf(args.verbosity) === -1 ? 2 : LEVELS.indexOf(args.verbosity);
50
+ const logAt = (index, method) =>
51
+ level >= index ? (...arguments_) => console[method](new Date().toISOString(), ...arguments_) : () => {};
52
+
53
+ const log = {
54
+ error: logAt(0, 'error'),
55
+ warn: logAt(1, 'warn'),
56
+ info: logAt(2, 'log'),
57
+ debug: logAt(3, 'log'),
58
+ };
59
+
60
+ const port = (value) => (value === undefined ? undefined : Number.parseInt(value, 10));
8
61
 
9
62
  const hmSim = new HmSim({
10
63
  log,
11
64
  devices: {
12
65
  rfd: require('./data/devices-rfd.json'),
13
- hmip: require('./data/devices-hmip.json')
66
+ hmip: require('./data/devices-hmip.json'),
14
67
  },
15
68
  config: {
16
- listenAddress: '127.0.0.1',
17
- binrpcListenPort: 2001,
18
- xmlrpcListenPort: 2010
19
- }
69
+ listenAddress: args['listen-address'],
70
+ binrpcListenPort: port(args['binrpc-port']),
71
+ xmlrpcListenPort: port(args['xmlrpc-port']),
72
+ virtualListenPort: port(args['virtual-port']),
73
+ cuxdListenPort: port(args['cuxd-port']),
74
+ },
75
+ rega: args['no-rega'] ? undefined : {port: port(args['rega-port']), listenAddress: args['listen-address']},
20
76
  });
77
+
78
+ for (const signal of ['SIGINT', 'SIGTERM']) {
79
+ process.on(signal, () => {
80
+ log.info('got', signal, '- shutting down');
81
+ hmSim.close();
82
+ process.exit(0);
83
+ });
84
+ }
@@ -0,0 +1,100 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Minimal binrpc response encoder.
5
+ *
6
+ * binrpc's own server only ever writes message type 0x01 (response); the real rfd/crRFD answers an
7
+ * invalid call with message type 0xff and a struct body holding faultCode and faultString. To be
8
+ * able to send both, the simulator encodes responses itself instead of reaching into binrpc's
9
+ * internals. The encoding mirrors binrpc's protocol module byte for byte (every 32 bit word
10
+ * big-endian, strings ascii, doubles as mantissa/exponent pair); `test/binrpc-codec.test.js`
11
+ * verifies that against a real binrpc client.
12
+ */
13
+
14
+ function word32(value) {
15
+ const buf = Buffer.alloc(4);
16
+ buf.writeUInt32BE(value >>> 0, 0);
17
+ return buf;
18
+ }
19
+
20
+ function encodeInteger(value) {
21
+ return Buffer.concat([word32(0x01), word32(value)]);
22
+ }
23
+
24
+ function encodeDouble(value) {
25
+ const exponent = Math.floor(Math.log(Math.abs(value)) / Math.LN2) + 1;
26
+ const mantissa = Math.floor(value * Math.pow(2, -exponent) * (1 << 30));
27
+ return Buffer.concat([word32(0x04), word32(mantissa), word32(exponent)]);
28
+ }
29
+
30
+ function encodeString(value) {
31
+ const str = String(value);
32
+ return Buffer.concat([word32(0x03), word32(str.length), Buffer.from(str, 'ascii')]);
33
+ }
34
+
35
+ function encodeBool(value) {
36
+ return Buffer.concat([word32(0x02), Buffer.from([value ? 1 : 0])]);
37
+ }
38
+
39
+ function encodeKey(key) {
40
+ return Buffer.concat([word32(key.length), Buffer.from(key, 'ascii')]);
41
+ }
42
+
43
+ function encodeData(data) {
44
+ if (data === undefined || data === null) {
45
+ return encodeString('');
46
+ }
47
+ switch (typeof data) {
48
+ case 'number':
49
+ return data % 1 === 0 ? encodeInteger(data) : encodeDouble(data);
50
+ case 'boolean':
51
+ return encodeBool(data);
52
+ case 'string':
53
+ return encodeString(data);
54
+ case 'object':
55
+ break;
56
+ default:
57
+ return encodeString(String(data));
58
+ }
59
+
60
+ if (Array.isArray(data)) {
61
+ return Buffer.concat([word32(0x100), word32(data.length), ...data.map((item) => encodeData(item))]);
62
+ }
63
+
64
+ if (typeof data.explicitDouble === 'number') {
65
+ return encodeDouble(data.explicitDouble);
66
+ }
67
+
68
+ const keys = Object.keys(data).filter((key) => data[key] !== undefined);
69
+ const members = [];
70
+ for (const key of keys) {
71
+ members.push(encodeKey(key), encodeData(data[key]));
72
+ }
73
+ return Buffer.concat([word32(0x101), word32(keys.length), ...members]);
74
+ }
75
+
76
+ function message(type, body) {
77
+ return Buffer.concat([Buffer.from('Bin', 'ascii'), Buffer.from([type]), word32(body.length), body]);
78
+ }
79
+
80
+ /**
81
+ * @param {*} data
82
+ * @returns {Buffer} message type 0x01
83
+ */
84
+ function encodeResponse(data) {
85
+ return message(0x01, encodeData(data === undefined ? '' : data));
86
+ }
87
+
88
+ /**
89
+ * @param {{faultCode: number, faultString: string}} fault
90
+ * @returns {Buffer} message type 0xff
91
+ */
92
+ function encodeFault(fault) {
93
+ const body = encodeData({
94
+ faultCode: Number(fault.faultCode) || 0,
95
+ faultString: fault.faultString === undefined ? '' : String(fault.faultString),
96
+ });
97
+ return message(0xff, body);
98
+ }
99
+
100
+ module.exports = {encodeData, encodeResponse, encodeFault};
package/lib/faults.js ADDED
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Fault responses.
5
+ *
6
+ * The real interface processes (rfd/crRFD, hmipserver, CUxD) answer invalid calls with an XML-RPC
7
+ * fault (binrpc message type 0xff with a struct body). The exact codes and strings are not
8
+ * publicly specified, so until 2026-09-05 the table below was an educated guess following the
9
+ * negative-code convention of the eq3 XML-RPC API.
10
+ *
11
+ * It is a measurement now. Homematic Manager's roadmap task 6 probed both interface processes of
12
+ * two CCUs on firmware 3.89.8 with calls that cannot change anything - an address that does not
13
+ * exist, a paramset that does not exist, a read-only parameter, a method that does not exist - and
14
+ * recorded what came back; `docs/config-pending.md` in that repository has the full tables and the
15
+ * script that produced them.
16
+ *
17
+ * The two processes disagree, and not only in wording:
18
+ *
19
+ * - hmipserver answers a fault for nearly everything and uses **-5 `Invalid parameter or value`**
20
+ * for every kind of bad `putParamset`, whatever went wrong.
21
+ * - rfd answers **no fault at all** for a whole class of mistakes: an unknown paramset name is
22
+ * taken as a peer address, a missing argument defaults, a write to a read-only datapoint is
23
+ * accepted. What it does fault on, it names differently.
24
+ *
25
+ * {@link DEFAULT_FAULTS} is hmipserver's table, because that is the one an application has to
26
+ * survive; {@link BIDCOS_FAULTS} is rfd's, for a simulator that models a BidCos interface. Every
27
+ * entry can still be overridden through the `faults` constructor option, and `sim.faults` exposes
28
+ * the effective table.
29
+ */
30
+
31
+ /** hmipserver 3.89.8, measured 2026-09-05. */
32
+ const DEFAULT_FAULTS = {
33
+ // no handler for the requested method name. hmipserver answers this at the HTTP level and
34
+ // sends no faultCode at all, so -1 is the simulator's stand-in
35
+ unknownMethod: {faultCode: -1, faultString: 'Invalid XML-RPC message'},
36
+ // device or channel address not known to this interface process
37
+ unknownInstance: {faultCode: -2, faultString: 'Invalid device'},
38
+ // the device/channel exists but does not have the requested paramset
39
+ unknownParamset: {faultCode: -2, faultString: 'Invalid device'},
40
+ // the paramset exists but does not contain the requested parameter (getValue/setValue)
41
+ unknownParameter: {faultCode: -5, faultString: 'Unknown Parameter for value key'},
42
+ // the two channels are not linked with each other
43
+ unknownLink: {faultCode: -2, faultString: 'Invalid device'},
44
+ // parameter exists but OPERATIONS does not include write (2)
45
+ readOnly: {faultCode: -5, faultString: 'Invalid parameter or value'},
46
+ // value has a type the parameter's TYPE does not accept
47
+ typeError: {faultCode: -5, faultString: 'Invalid parameter or value'},
48
+ // ENUM index/name not in VALUE_LIST. hmipserver does *not* check MIN..MAX at all - see the
49
+ // 'hmip' configPendingMode in lib/sim.js
50
+ outOfRange: {faultCode: -5, faultString: 'Invalid parameter or value'},
51
+ // the stored configuration of a channel cannot be transferred to the device: the fault a
52
+ // poisoned channel answers to every putParamset, including one with an empty struct
53
+ invalidValue: {faultCode: -5, faultString: 'Invalid parameter or value'},
54
+ // method understood but not implemented for this interface: clearConfigCache,
55
+ // restoreConfigToDevice and determineParameter all answer this on hmipserver
56
+ notSupported: {faultCode: -1, faultString: 'Generic error'},
57
+ // device is known but currently not reachable (a sleeping battery device)
58
+ notReachable: {faultCode: -1, faultString: 'Generic error (UNREACH)'},
59
+ // malformed parameters. hmipserver lets a Java NullPointerException through here and answers
60
+ // -321 with the exception message, which is not something an application can rely on
61
+ invalidArguments: {faultCode: -321, faultString: 'Invalid arguments'},
62
+ };
63
+
64
+ /** rfd (BidCos-RF) 3.89.8, measured 2026-09-05. */
65
+ const BIDCOS_FAULTS = {
66
+ unknownMethod: {faultCode: -1, faultString: 'unknown method name'},
67
+ unknownInstance: {faultCode: -2, faultString: 'Unknown instance'},
68
+ // rfd does not fault here at all: an unknown paramset name is taken as a peer channel address
69
+ // and the link defaults come back. The entry covers what the simulator cannot model.
70
+ unknownParamset: {faultCode: -2, faultString: 'Unknown instance'},
71
+ unknownParameter: {faultCode: -5, faultString: 'Unknown parameter'},
72
+ unknownLink: {faultCode: -1, faultString: 'Failure'},
73
+ // rfd accepts a setValue on a read-only datapoint, so this is never raised against it
74
+ readOnly: {faultCode: -5, faultString: 'Unknown parameter'},
75
+ typeError: {faultCode: -5, faultString: 'Unknown parameter'},
76
+ outOfRange: {faultCode: -5, faultString: 'Unknown parameter'},
77
+ invalidValue: {faultCode: -5, faultString: 'Unknown parameter'},
78
+ notSupported: {faultCode: -1, faultString: 'Failure'},
79
+ notReachable: {faultCode: -1, faultString: 'Failure'},
80
+ invalidArguments: {faultCode: -1, faultString: 'Failure'},
81
+ };
82
+
83
+ /** The measured tables by name, for `new HmSim({faults: FAULT_TABLES.bidcos})`. */
84
+ const FAULT_TABLES = {hmip: DEFAULT_FAULTS, bidcos: BIDCOS_FAULTS};
85
+
86
+ /** Error carrying an XML-RPC fault. */
87
+ class RpcFault extends Error {
88
+ constructor(faultCode, faultString, detail) {
89
+ super(detail ? `${faultString}: ${detail}` : faultString);
90
+ this.name = 'RpcFault';
91
+ this.faultCode = faultCode;
92
+ this.faultString = faultString;
93
+ this.detail = detail;
94
+ }
95
+
96
+ /** The struct a binrpc fault carries as its body. */
97
+ toStruct() {
98
+ return {faultCode: this.faultCode, faultString: this.faultString};
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Builds a fault factory bound to a (possibly overridden) fault table.
104
+ * @param {object} [overrides] partial table, same shape as DEFAULT_FAULTS
105
+ * @returns {{table: object, fault: function(string, string=): RpcFault}}
106
+ */
107
+ function createFaults(overrides = {}) {
108
+ const table = {};
109
+ for (const [key, value] of Object.entries(DEFAULT_FAULTS)) {
110
+ table[key] = {...value, ...overrides[key]};
111
+ }
112
+ for (const [key, value] of Object.entries(overrides)) {
113
+ if (!table[key]) {
114
+ table[key] = {...value};
115
+ }
116
+ }
117
+
118
+ const fault = (key, detail) => {
119
+ const entry = table[key] || table.unknownMethod;
120
+ return new RpcFault(entry.faultCode, entry.faultString, detail);
121
+ };
122
+
123
+ return {table, fault};
124
+ }
125
+
126
+ module.exports = {DEFAULT_FAULTS, BIDCOS_FAULTS, FAULT_TABLES, RpcFault, createFaults};
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Answers to `system.methodHelp`. The real interface processes answer with a one line signature;
5
+ * the wording here follows the eq3 XML-RPC API documentation.
6
+ */
7
+ module.exports = {
8
+ 'system.listMethods': 'array system.listMethods() - names of all methods this interface knows',
9
+ 'system.methodHelp': 'string system.methodHelp(string method) - the documentation of a method',
10
+ 'system.multicall': 'array system.multicall(array calls) - execute several method calls at once',
11
+ init: 'string init(string url, string interfaceId) - register or (with an empty id) remove a logic layer',
12
+ listDevices: 'array listDevices() - descriptions of all devices and channels',
13
+ getDeviceDescription: 'struct getDeviceDescription(string address) - description of one device or channel',
14
+ getParamsetDescription:
15
+ 'struct getParamsetDescription(string address, string paramsetType) - description of a paramset',
16
+ getParamsetId: 'string getParamsetId(string address, string paramsetType) - identifier of a paramset',
17
+ getParamset: 'struct getParamset(string address, string paramsetKey) - values of a paramset',
18
+ putParamset: 'void putParamset(string address, string paramsetKey, struct set) - write values of a paramset',
19
+ getValue: 'ValueType getValue(string address, string valueKey) - read one value',
20
+ setValue: 'void setValue(string address, string valueKey, ValueType value) - write one value',
21
+ ping: 'boolean ping(string callerId) - triggers a PONG event',
22
+ getLinks: 'array getLinks(string address, int flags) - links of a channel or of the whole interface',
23
+ getLinkInfo: 'struct getLinkInfo(string senderAddress, string receiverAddress) - name and description of a link',
24
+ setLinkInfo:
25
+ 'void setLinkInfo(string senderAddress, string receiverAddress, string name, string description) - rename a link',
26
+ getLinkPeers: 'array getLinkPeers(string address) - the peers a channel is linked with',
27
+ addLink:
28
+ 'void addLink(string senderAddress, string receiverAddress, string name, string description) - link two channels',
29
+ removeLink: 'void removeLink(string senderAddress, string receiverAddress) - remove a link',
30
+ activateLinkParamset:
31
+ 'void activateLinkParamset(string address, string peerAddress, boolean longPress) - apply a link paramset',
32
+ rssiInfo: 'struct rssiInfo() - receive levels of all devices',
33
+ listBidcosInterfaces: 'array listBidcosInterfaces() - the BidCos interfaces (antennas) of this process',
34
+ setBidcosInterface:
35
+ 'void setBidcosInterface(string deviceId, string interfaceId, boolean roaming) - assign a device to an interface',
36
+ getServiceMessages: 'array getServiceMessages() - the pending service messages',
37
+ setInstallMode: 'void setInstallMode(boolean on, int time, int mode) - switch the install mode',
38
+ getInstallMode: 'int getInstallMode() - remaining seconds of the install mode',
39
+ deleteDevice: 'void deleteDevice(string address, int flags) - remove a device',
40
+ replaceDevice:
41
+ 'void replaceDevice(string oldDeviceAddress, string newDeviceAddress) - take over the configuration of a device',
42
+ reportValueUsage:
43
+ 'void reportValueUsage(string address, string valueId, int refCounter) - announce datapoint usage',
44
+ updateFirmware: 'boolean updateFirmware(array deviceAddresses) - start a firmware update',
45
+ installFirmware: 'boolean installFirmware(string deviceAddress) - install a pending firmware',
46
+ clearConfigCache: 'void clearConfigCache(string address) - drop the cached configuration of a device',
47
+ restoreConfigToDevice: 'void restoreConfigToDevice(string address) - write the cached configuration to the device',
48
+ determineParameter:
49
+ 'void determineParameter(string address, string paramsetKey, string parameterId) - read a parameter from the device',
50
+ };
package/lib/rega.js ADDED
@@ -0,0 +1,245 @@
1
+ 'use strict';
2
+
3
+ const http = require('node:http');
4
+
5
+ /**
6
+ * ReGa (HomeMatic logic layer) mock.
7
+ *
8
+ * Answers the `rega.exe` endpoint the way the CCU does: the script output followed by an `<xml>`
9
+ * block. The scripts of the `homematic-rega` client are recognised by their first line
10
+ * (`!# devices.rega`, `!# variables.rega`, `!# programs.rega`, `!# rooms.rega`,
11
+ * `!# functions.rega`, `!# values.rega`); `getChannels()` uses `channels.rega`, whose first line is
12
+ * the historical `!# devices.rega`.
13
+ *
14
+ * Every script that is not one of those is echoed into `regaSim.scripts` (and, for the ones the
15
+ * mock understands, applied to the mock's state), so tests can assert on what was executed -
16
+ * renames (`dom.GetObject(id).Name("...")`) in particular.
17
+ */
18
+ class RegaSim {
19
+ /**
20
+ * @param {object} options
21
+ * @param {number} [options.port=8181]
22
+ * @param {string} [options.listenAddress]
23
+ * @param {Array} [options.variables]
24
+ * @param {Array} [options.channels] devices and channels, the answer to `getChannels()`
25
+ * @param {Array} [options.programs]
26
+ * @param {Array} [options.rooms]
27
+ * @param {Array} [options.functions]
28
+ * @param {Array} [options.values] the answer to `getValues()`
29
+ * @param {object} [options.auth] {username, password} - enables HTTP basic auth
30
+ * @param {object} [options.tls] {key, cert} - serve https instead of http
31
+ * @param {object} [log]
32
+ */
33
+ constructor(options, log) {
34
+ this.log = log || {debug() {}, info() {}, warn() {}, error() {}};
35
+ this.variables = options.variables || [];
36
+ this.devices = options.channels || [];
37
+ this.programs = options.programs || [];
38
+ this.rooms = options.rooms || [];
39
+ this.functions = options.functions || [];
40
+ this.values = options.values || [];
41
+ /** every script the mock received, newest last */
42
+ this.scripts = [];
43
+ /** every rename script, as {id, name, script} */
44
+ this.renames = [];
45
+ this.auth = options.auth || null;
46
+
47
+ const handler = (request, response) => this.handleRequest(request, response);
48
+ this.server = options.tls
49
+ ? require('node:https').createServer(options.tls, handler)
50
+ : http.createServer(handler);
51
+ /** the port the mock listens on - known once `ready` resolved, which matters for port 0 */
52
+ this.port = options.port === undefined ? 8181 : options.port;
53
+ /** resolves once the mock accepts connections, rejects when the port is taken */
54
+ this.ready = new Promise((resolve, reject) => {
55
+ this.server.on('error', reject);
56
+ this.server.listen(this.port, options.listenAddress, () => {
57
+ const address = this.server.address();
58
+ if (address) {
59
+ this.port = address.port;
60
+ }
61
+ resolve();
62
+ });
63
+ });
64
+ }
65
+
66
+ handleRequest(request, response) {
67
+ if (this.auth) {
68
+ const expected =
69
+ 'Basic ' + Buffer.from(`${this.auth.username}:${this.auth.password || ''}`, 'utf8').toString('base64');
70
+ if (request.headers.authorization !== expected) {
71
+ response.writeHead(401, {
72
+ 'WWW-Authenticate': 'Basic realm="hm-simulator"',
73
+ 'Content-Type': 'text/plain',
74
+ });
75
+ response.end('Unauthorized');
76
+ return;
77
+ }
78
+ }
79
+
80
+ const path = (request.url || '/').split('?')[0];
81
+ if (!/^\/[\w-]+\.exe$/.test(path)) {
82
+ response.writeHead(404, {'Content-Type': 'text/plain'});
83
+ response.end('Not Found');
84
+ return;
85
+ }
86
+
87
+ if (request.method !== 'POST') {
88
+ this.send(response, this.response(''));
89
+ return;
90
+ }
91
+
92
+ const chunks = [];
93
+ request.on('data', (chunk) => chunks.push(chunk));
94
+ request.on('end', () => {
95
+ const script = Buffer.concat(chunks).toString('latin1');
96
+ this.send(response, this.response(this.exec(script)));
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Runs a script against the mock's state.
102
+ * @param {string} script
103
+ * @returns {string|Array|object} the script's output
104
+ */
105
+ exec(script) {
106
+ this.scripts.push(script);
107
+ const line = script.split('\n')[0].trim();
108
+
109
+ switch (line) {
110
+ case '!# devices.rega':
111
+ return this.devices;
112
+ case '!# variables.rega':
113
+ return this.variables;
114
+ case '!# programs.rega':
115
+ return this.programs;
116
+ case '!# rooms.rega':
117
+ return this.rooms;
118
+ case '!# functions.rega':
119
+ return this.functions;
120
+ case '!# values.rega':
121
+ return this.values;
122
+ default:
123
+ return this.execStatement(script, line);
124
+ }
125
+ }
126
+
127
+ execStatement(script, line) {
128
+ let match = line.match(/dom\.GetObject\((\d+)\)\.State\(([^)]*)\)/);
129
+ if (match) {
130
+ const id = Number.parseInt(match[1], 10);
131
+ let value;
132
+ try {
133
+ value = JSON.parse(match[2]);
134
+ } catch {
135
+ value = match[2];
136
+ }
137
+ for (const [index, variable] of this.variables.entries()) {
138
+ if (variable.id === id) {
139
+ this.variables[index].val = value;
140
+ this.variables[index].ts = this.ts();
141
+ }
142
+ }
143
+ return '';
144
+ }
145
+
146
+ match = line.match(/dom\.GetObject\((\d+)\)\.Name\("([^"]*)"\)/);
147
+ if (match) {
148
+ const id = Number.parseInt(match[1], 10);
149
+ const name = match[2];
150
+ this.renames.push({id, name, script});
151
+ for (const list of [this.devices, this.variables, this.programs, this.rooms, this.functions]) {
152
+ for (const item of list) {
153
+ if (item.id === id) {
154
+ item.name = name;
155
+ }
156
+ }
157
+ }
158
+ return '';
159
+ }
160
+
161
+ match = line.match(/dom\.GetObject\((\d+)\)\.Active\((true|false)\)/);
162
+ if (match) {
163
+ const id = Number.parseInt(match[1], 10);
164
+ for (const program of this.programs) {
165
+ if (program.id === id) {
166
+ program.active = match[2] === 'true';
167
+ program.ts = this.ts();
168
+ }
169
+ }
170
+ return '';
171
+ }
172
+
173
+ match = line.match(/dom\.GetObject\((\d+)\)\.ProgramExecute\(\)/);
174
+ if (match) {
175
+ const id = Number.parseInt(match[1], 10);
176
+ for (const program of this.programs) {
177
+ if (program.id === id) {
178
+ program.ts = this.ts();
179
+ }
180
+ }
181
+ return '';
182
+ }
183
+
184
+ this.log.warn('unknown script', line);
185
+ return '';
186
+ }
187
+
188
+ send(response, body) {
189
+ const buffer = Buffer.from(body, 'latin1');
190
+ response.writeHead(200, {
191
+ server: 'ise GmbH HTTP-Server v2.0',
192
+ 'accept-ranges': 'bytes',
193
+ 'cache-control': 'no-store, no-cache',
194
+ 'content-type': 'text/xml; charset=iso-8859-1',
195
+ 'content-length': buffer.length,
196
+ });
197
+ response.end(buffer);
198
+ }
199
+
200
+ close() {
201
+ this.server.close();
202
+ if (typeof this.server.closeAllConnections === 'function') {
203
+ this.server.closeAllConnections();
204
+ }
205
+ }
206
+
207
+ response(stdout, vars = []) {
208
+ if (typeof stdout !== 'string') {
209
+ stdout = JSON.stringify(stdout);
210
+ }
211
+
212
+ const xml = [
213
+ ['exec', 'rega.exe'],
214
+ ['sessionId', ''],
215
+ ['httpUserAgent', ''],
216
+ ].concat(vars);
217
+
218
+ stdout += '<xml>';
219
+ for (const [key, value] of xml) {
220
+ stdout += `<${key}>${value}</${key}>`;
221
+ }
222
+ stdout += '</xml>';
223
+
224
+ return stdout;
225
+ }
226
+
227
+ ts() {
228
+ const d = new Date();
229
+ return (
230
+ d.getFullYear() +
231
+ '-' +
232
+ ('0' + (d.getMonth() + 1)).slice(-2) +
233
+ '-' +
234
+ ('0' + d.getDate()).slice(-2) +
235
+ ' ' +
236
+ ('0' + d.getHours()).slice(-2) +
237
+ ':' +
238
+ ('0' + d.getMinutes()).slice(-2) +
239
+ ':' +
240
+ ('0' + d.getSeconds()).slice(-2)
241
+ );
242
+ }
243
+ }
244
+
245
+ module.exports = RegaSim;