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/LICENSE +21 -0
- package/README.md +330 -67
- package/behaviors/virtual-button-press.js +1 -1
- package/behaviors/window-open-close.js +1 -1
- package/data/fixtures/devices.json +31606 -0
- package/data/fixtures/lab-devices.json +4563 -0
- package/index.js +72 -7
- package/lib/binrpc-codec.js +100 -0
- package/lib/faults.js +126 -0
- package/lib/method-help.js +50 -0
- package/lib/rega.js +245 -0
- package/lib/servers.js +150 -0
- package/lib/sim.js +1751 -0
- package/lib/tls.js +160 -0
- package/lib/validate.js +233 -0
- package/package.json +31 -22
- package/sim.js +3 -541
- package/sim.mjs +5 -0
- package/.travis.yml +0 -3
- package/config.js +0 -3
package/lib/tls.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Self signed certificate, generated at start.
|
|
7
|
+
*
|
|
8
|
+
* The CCU serves its XML-RPC interfaces over TLS with a certificate nobody can validate either, so
|
|
9
|
+
* a client that talks to the simulator has to be able to deal with exactly that. Node can generate
|
|
10
|
+
* key pairs but not certificates, and a test simulator should not need a dependency (or a private
|
|
11
|
+
* key checked into the repository) for it, so the certificate is DER encoded here by hand. It is a
|
|
12
|
+
* plain v3 certificate for localhost/127.0.0.1, valid for one year, and it is only ever meant for
|
|
13
|
+
* `rejectUnauthorized: false` clients.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
function length(size) {
|
|
17
|
+
if (size < 0x80) {
|
|
18
|
+
return Buffer.from([size]);
|
|
19
|
+
}
|
|
20
|
+
const bytes = [];
|
|
21
|
+
let rest = size;
|
|
22
|
+
while (rest > 0) {
|
|
23
|
+
bytes.unshift(rest & 0xff);
|
|
24
|
+
rest >>= 8;
|
|
25
|
+
}
|
|
26
|
+
return Buffer.from([0x80 | bytes.length, ...bytes]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const tagged = (tag, content) => Buffer.concat([Buffer.from([tag]), length(content.length), content]);
|
|
30
|
+
const sequence = (...parts) => tagged(0x30, Buffer.concat(parts));
|
|
31
|
+
const set = (content) => tagged(0x31, content);
|
|
32
|
+
const octetString = (content) => tagged(0x04, content);
|
|
33
|
+
const bitString = (content) => tagged(0x03, Buffer.concat([Buffer.from([0x00]), content]));
|
|
34
|
+
const utf8String = (value) => tagged(0x0c, Buffer.from(value, 'utf8'));
|
|
35
|
+
const boolean = (value) => tagged(0x01, Buffer.from([value ? 0xff : 0x00]));
|
|
36
|
+
const nullValue = () => Buffer.from([0x05, 0x00]);
|
|
37
|
+
|
|
38
|
+
function integer(value) {
|
|
39
|
+
let bytes = [];
|
|
40
|
+
let rest = BigInt(value);
|
|
41
|
+
if (rest === 0n) {
|
|
42
|
+
bytes = [0];
|
|
43
|
+
}
|
|
44
|
+
while (rest > 0n) {
|
|
45
|
+
bytes.unshift(Number(rest & 0xffn));
|
|
46
|
+
rest >>= 8n;
|
|
47
|
+
}
|
|
48
|
+
if (bytes[0] & 0x80) {
|
|
49
|
+
bytes.unshift(0);
|
|
50
|
+
}
|
|
51
|
+
return tagged(0x02, Buffer.from(bytes));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function oid(dotted) {
|
|
55
|
+
const parts = dotted.split('.').map(Number);
|
|
56
|
+
const bytes = [parts[0] * 40 + parts[1]];
|
|
57
|
+
for (const part of parts.slice(2)) {
|
|
58
|
+
const chunk = [part & 0x7f];
|
|
59
|
+
let rest = part >> 7;
|
|
60
|
+
while (rest > 0) {
|
|
61
|
+
chunk.unshift((rest & 0x7f) | 0x80);
|
|
62
|
+
rest >>= 7;
|
|
63
|
+
}
|
|
64
|
+
bytes.push(...chunk);
|
|
65
|
+
}
|
|
66
|
+
return tagged(0x06, Buffer.from(bytes));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function utcTime(date) {
|
|
70
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
71
|
+
const text =
|
|
72
|
+
pad(date.getUTCFullYear() % 100) +
|
|
73
|
+
pad(date.getUTCMonth() + 1) +
|
|
74
|
+
pad(date.getUTCDate()) +
|
|
75
|
+
pad(date.getUTCHours()) +
|
|
76
|
+
pad(date.getUTCMinutes()) +
|
|
77
|
+
pad(date.getUTCSeconds()) +
|
|
78
|
+
'Z';
|
|
79
|
+
return tagged(0x17, Buffer.from(text, 'ascii'));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Name with a single CN attribute. */
|
|
83
|
+
const name = (commonName) => sequence(set(sequence(oid('2.5.4.3'), utf8String(commonName))));
|
|
84
|
+
|
|
85
|
+
const extension = (id, critical, value) =>
|
|
86
|
+
critical ? sequence(oid(id), boolean(true), octetString(value)) : sequence(oid(id), octetString(value));
|
|
87
|
+
|
|
88
|
+
function subjectAltName(hostnames, ips) {
|
|
89
|
+
const parts = [
|
|
90
|
+
...hostnames.map((host) => tagged(0x82, Buffer.from(host, 'ascii'))),
|
|
91
|
+
...ips.map((ip) => tagged(0x87, Buffer.from(ip.split('.').map(Number)))),
|
|
92
|
+
];
|
|
93
|
+
return sequence(...parts);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {object} [options]
|
|
98
|
+
* @param {string} [options.commonName='localhost']
|
|
99
|
+
* @param {Array<string>} [options.hostnames=['localhost']]
|
|
100
|
+
* @param {Array<string>} [options.ips=['127.0.0.1']]
|
|
101
|
+
* @param {number} [options.days=365]
|
|
102
|
+
* @returns {{key: string, cert: string}} both PEM encoded
|
|
103
|
+
*/
|
|
104
|
+
function selfSignedCertificate({
|
|
105
|
+
commonName = 'localhost',
|
|
106
|
+
hostnames = ['localhost'],
|
|
107
|
+
ips = ['127.0.0.1'],
|
|
108
|
+
days = 365,
|
|
109
|
+
} = {}) {
|
|
110
|
+
const {privateKey, publicKey} = crypto.generateKeyPairSync('rsa', {modulusLength: 2048});
|
|
111
|
+
const spki = publicKey.export({type: 'spki', format: 'der'});
|
|
112
|
+
|
|
113
|
+
const sha256WithRsa = sequence(oid('1.2.840.113549.1.1.11'), nullValue());
|
|
114
|
+
const notBefore = new Date(Date.now() - 60 * 60 * 1000);
|
|
115
|
+
const notAfter = new Date(Date.now() + days * 24 * 60 * 60 * 1000);
|
|
116
|
+
|
|
117
|
+
const tbs = sequence(
|
|
118
|
+
tagged(0xa0, integer(2)), // version v3
|
|
119
|
+
integer(Date.now()),
|
|
120
|
+
sha256WithRsa,
|
|
121
|
+
name(commonName),
|
|
122
|
+
sequence(utcTime(notBefore), utcTime(notAfter)),
|
|
123
|
+
name(commonName),
|
|
124
|
+
spki,
|
|
125
|
+
tagged(
|
|
126
|
+
0xa3,
|
|
127
|
+
sequence(
|
|
128
|
+
extension('2.5.29.19', true, sequence(boolean(true))), // basicConstraints CA:TRUE
|
|
129
|
+
extension('2.5.29.17', false, subjectAltName(hostnames, ips)),
|
|
130
|
+
),
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const signature = crypto.sign('sha256', tbs, privateKey);
|
|
135
|
+
const der = sequence(tbs, sha256WithRsa, bitString(signature));
|
|
136
|
+
|
|
137
|
+
const base64 = der.toString('base64').replace(/(.{64})/g, '$1\n');
|
|
138
|
+
const cert = `-----BEGIN CERTIFICATE-----\n${base64}${base64.endsWith('\n') ? '' : '\n'}-----END CERTIFICATE-----\n`;
|
|
139
|
+
|
|
140
|
+
return {key: privateKey.export({type: 'pkcs8', format: 'pem'}), cert};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Turns the `tls` option into {key, cert}: `true` generates a certificate, an object with key and
|
|
145
|
+
* cert is used as it is.
|
|
146
|
+
*/
|
|
147
|
+
function resolveTls(option) {
|
|
148
|
+
if (!option) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
if (option === true) {
|
|
152
|
+
return selfSignedCertificate();
|
|
153
|
+
}
|
|
154
|
+
if (option.key && option.cert) {
|
|
155
|
+
return option;
|
|
156
|
+
}
|
|
157
|
+
return selfSignedCertificate(option);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {selfSignedCertificate, resolveTls};
|
package/lib/validate.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type and range checking of parameter values against a paramset description.
|
|
5
|
+
*
|
|
6
|
+
* The real interface processes are stricter than the old simulator was: a value of the wrong type
|
|
7
|
+
* or outside MIN..MAX is answered with a fault (hmipserver) or silently dropped and left in
|
|
8
|
+
* CONFIG_PENDING (crRFD, see the CONFIG_PENDING modes in lib/sim.js). Both need the same check,
|
|
9
|
+
* so it lives here.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const WRITEABLE = 2;
|
|
13
|
+
|
|
14
|
+
/** ENUM values arrive as the index (BidCos) or as the name (HmIP); both are stored as the index. */
|
|
15
|
+
function castEnum(parameter, value, fault, name) {
|
|
16
|
+
const list = Array.isArray(parameter.VALUE_LIST) ? parameter.VALUE_LIST : [];
|
|
17
|
+
if (typeof value === 'string') {
|
|
18
|
+
const index = list.indexOf(value);
|
|
19
|
+
if (index === -1) {
|
|
20
|
+
throw fault('outOfRange', `${name}=${value}`);
|
|
21
|
+
}
|
|
22
|
+
return index;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
|
26
|
+
throw fault('typeError', `${name} is ENUM`);
|
|
27
|
+
}
|
|
28
|
+
if (value < 0 || (list.length > 0 && value >= list.length)) {
|
|
29
|
+
throw fault('outOfRange', `${name}=${value}`);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function castNumber(parameter, value, fault, name) {
|
|
35
|
+
if (value !== null && typeof value === 'object' && typeof value.explicitDouble === 'number') {
|
|
36
|
+
value = value.explicitDouble;
|
|
37
|
+
}
|
|
38
|
+
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
39
|
+
throw fault('typeError', `${name} is ${parameter.TYPE}`);
|
|
40
|
+
}
|
|
41
|
+
if (parameter.TYPE === 'INTEGER' && !Number.isInteger(value)) {
|
|
42
|
+
throw fault('typeError', `${name} is INTEGER`);
|
|
43
|
+
}
|
|
44
|
+
if (typeof parameter.MIN === 'number' && value < parameter.MIN) {
|
|
45
|
+
throw fault('outOfRange', `${name}=${value} < ${parameter.MIN}`);
|
|
46
|
+
}
|
|
47
|
+
if (typeof parameter.MAX === 'number' && value > parameter.MAX) {
|
|
48
|
+
throw fault('outOfRange', `${name}=${value} > ${parameter.MAX}`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {object} description the paramset description
|
|
55
|
+
* @param {string} name parameter name
|
|
56
|
+
* @param {*} value
|
|
57
|
+
* @param {function} fault fault factory, see lib/faults.js
|
|
58
|
+
* @param {object} [options]
|
|
59
|
+
* @param {boolean} [options.write=true] check OPERATIONS for the write bit
|
|
60
|
+
* @returns {*} the value as it is stored
|
|
61
|
+
*/
|
|
62
|
+
function castValue(description, name, value, fault, {write = true} = {}) {
|
|
63
|
+
const parameter = description && description[name];
|
|
64
|
+
if (!parameter) {
|
|
65
|
+
throw fault('unknownParameter', name);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (write && typeof parameter.OPERATIONS === 'number' && !(parameter.OPERATIONS & WRITEABLE)) {
|
|
69
|
+
throw fault('readOnly', name);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
switch (parameter.TYPE) {
|
|
73
|
+
case 'BOOL':
|
|
74
|
+
case 'ACTION':
|
|
75
|
+
if (typeof value === 'boolean') {
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
// the interface processes accept 0/1 for BOOL as well
|
|
79
|
+
if (value === 0 || value === 1) {
|
|
80
|
+
return Boolean(value);
|
|
81
|
+
}
|
|
82
|
+
throw fault('typeError', `${name} is ${parameter.TYPE}`);
|
|
83
|
+
case 'INTEGER':
|
|
84
|
+
case 'FLOAT':
|
|
85
|
+
return castNumber(parameter, value, fault, name);
|
|
86
|
+
case 'ENUM':
|
|
87
|
+
return castEnum(parameter, value, fault, name);
|
|
88
|
+
case 'STRING':
|
|
89
|
+
if (typeof value !== 'string') {
|
|
90
|
+
throw fault('typeError', `${name} is STRING`);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
default:
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The value a parameter has before anything was written, taken from its description. */
|
|
99
|
+
function defaultValue(parameter) {
|
|
100
|
+
if (parameter.TYPE === 'ENUM' && Array.isArray(parameter.VALUE_LIST)) {
|
|
101
|
+
const index = parameter.VALUE_LIST.indexOf(parameter.DEFAULT);
|
|
102
|
+
return index === -1 ? 0 : index;
|
|
103
|
+
}
|
|
104
|
+
return parameter.DEFAULT;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Every parameter of a description with its default value. */
|
|
108
|
+
function defaultParamset(description) {
|
|
109
|
+
const result = {};
|
|
110
|
+
for (const [name, parameter] of Object.entries(description || {})) {
|
|
111
|
+
result[name] = defaultValue(parameter);
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Unwraps the `{explicitDouble}` wrapper both RPC encoders understand. */
|
|
117
|
+
function plain(value) {
|
|
118
|
+
return value !== null && typeof value === 'object' && typeof value.explicitDouble === 'number'
|
|
119
|
+
? value.explicitDouble
|
|
120
|
+
: value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function clamp(parameter, value) {
|
|
124
|
+
let result = value;
|
|
125
|
+
if (typeof parameter.MIN === 'number' && result < parameter.MIN) {
|
|
126
|
+
result = parameter.MIN;
|
|
127
|
+
}
|
|
128
|
+
if (typeof parameter.MAX === 'number' && result > parameter.MAX) {
|
|
129
|
+
result = parameter.MAX;
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* hmipserver's MASTER write, measured on firmware 3.89.8 (Homematic Manager task 6).
|
|
136
|
+
*
|
|
137
|
+
* It stores what it is given **before** it validates, and it validates the *type* only: a string
|
|
138
|
+
* in an INTEGER and an ENUM value that is not in the VALUE_LIST are stored as they came and make
|
|
139
|
+
* the channel's configuration invalid, while a number outside MIN..MAX is simply accepted - there
|
|
140
|
+
* is no range check anywhere in hmipserver.
|
|
141
|
+
*
|
|
142
|
+
* @param {object} parameter the parameter's description
|
|
143
|
+
* @param {*} value
|
|
144
|
+
* @returns {{value: *, valid: boolean}} the value as it is stored, and whether it is acceptable
|
|
145
|
+
*/
|
|
146
|
+
function hmipStoredValue(parameter, value) {
|
|
147
|
+
const raw = plain(value);
|
|
148
|
+
switch (parameter.TYPE) {
|
|
149
|
+
case 'BOOL':
|
|
150
|
+
case 'ACTION':
|
|
151
|
+
if (typeof raw === 'boolean') {
|
|
152
|
+
return {value: raw, valid: true};
|
|
153
|
+
}
|
|
154
|
+
if (raw === 0 || raw === 1) {
|
|
155
|
+
return {value: Boolean(raw), valid: true};
|
|
156
|
+
}
|
|
157
|
+
return {value: raw, valid: false};
|
|
158
|
+
case 'INTEGER':
|
|
159
|
+
return {value: raw, valid: typeof raw === 'number' && Number.isInteger(raw)};
|
|
160
|
+
case 'FLOAT':
|
|
161
|
+
// an <int> is accepted where a <double> belongs and stored as the integer it is
|
|
162
|
+
return {value: raw, valid: typeof raw === 'number' && !Number.isNaN(raw)};
|
|
163
|
+
case 'ENUM': {
|
|
164
|
+
const list = Array.isArray(parameter.VALUE_LIST) ? parameter.VALUE_LIST : [];
|
|
165
|
+
if (typeof raw === 'string') {
|
|
166
|
+
const index = list.indexOf(raw);
|
|
167
|
+
return index === -1 ? {value: raw, valid: false} : {value: index, valid: true};
|
|
168
|
+
}
|
|
169
|
+
const inRange = typeof raw === 'number' && Number.isInteger(raw) && raw >= 0 && raw < list.length;
|
|
170
|
+
return {value: raw, valid: inRange};
|
|
171
|
+
}
|
|
172
|
+
case 'STRING':
|
|
173
|
+
return {value: raw, valid: typeof raw === 'string'};
|
|
174
|
+
default:
|
|
175
|
+
return {value: raw, valid: true};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* rfd's MASTER write, measured on firmware 3.89.8 (Homematic Manager task 6).
|
|
181
|
+
*
|
|
182
|
+
* rfd never faults on a bad value. It silently clamps a number to MIN..MAX, coerces a string to a
|
|
183
|
+
* number, and ignores what it cannot use at all - an integer where a BOOL belongs, an ENUM name
|
|
184
|
+
* that is not in the VALUE_LIST. `undefined` means "ignored, nothing was stored".
|
|
185
|
+
*
|
|
186
|
+
* Not modelled: rfd also ignores a FLOAT that arrives as an XML-RPC `<int>`, which is why every
|
|
187
|
+
* FLOAT has to go out as an explicit double. Both decode to a JavaScript number, so the
|
|
188
|
+
* distinction is gone by the time the simulator sees the value.
|
|
189
|
+
*
|
|
190
|
+
* @param {object} parameter the parameter's description
|
|
191
|
+
* @param {*} value
|
|
192
|
+
* @returns {*} the value as it is stored, or `undefined` when rfd ignores it
|
|
193
|
+
*/
|
|
194
|
+
function bidcosStoredValue(parameter, value) {
|
|
195
|
+
const raw = plain(value);
|
|
196
|
+
switch (parameter.TYPE) {
|
|
197
|
+
case 'BOOL':
|
|
198
|
+
case 'ACTION':
|
|
199
|
+
return typeof raw === 'boolean' ? raw : undefined;
|
|
200
|
+
case 'INTEGER': {
|
|
201
|
+
const number = typeof raw === 'number' ? raw : Number.parseInt(String(raw), 10);
|
|
202
|
+
return clamp(parameter, Number.isNaN(number) ? 0 : Math.round(number));
|
|
203
|
+
}
|
|
204
|
+
case 'FLOAT': {
|
|
205
|
+
const number = typeof raw === 'number' ? raw : Number.parseFloat(String(raw));
|
|
206
|
+
return clamp(parameter, Number.isNaN(number) ? 0 : number);
|
|
207
|
+
}
|
|
208
|
+
case 'ENUM': {
|
|
209
|
+
const list = Array.isArray(parameter.VALUE_LIST) ? parameter.VALUE_LIST : [];
|
|
210
|
+
if (typeof raw === 'string') {
|
|
211
|
+
const index = list.indexOf(raw);
|
|
212
|
+
return index === -1 ? undefined : index;
|
|
213
|
+
}
|
|
214
|
+
if (typeof raw !== 'number' || !Number.isInteger(raw)) {
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
return list.length > 0 ? Math.min(Math.max(raw, 0), list.length - 1) : raw;
|
|
218
|
+
}
|
|
219
|
+
case 'STRING':
|
|
220
|
+
return typeof raw === 'string' ? raw : undefined;
|
|
221
|
+
default:
|
|
222
|
+
return raw;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = {
|
|
227
|
+
WRITEABLE,
|
|
228
|
+
castValue,
|
|
229
|
+
defaultValue,
|
|
230
|
+
defaultParamset,
|
|
231
|
+
hmipStoredValue,
|
|
232
|
+
bidcosStoredValue,
|
|
233
|
+
};
|
package/package.json
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hm-simulator",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Simulates a Homematic CCU",
|
|
5
|
-
"main": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Simulates a Homematic CCU (rfd, hmipserver, VirtualDevices, CUxD, ReGa) for automated tests",
|
|
5
|
+
"main": "sim.js",
|
|
6
6
|
"preferGlobal": true,
|
|
7
7
|
"bin": {
|
|
8
8
|
"hm-simulator": "./index.js"
|
|
9
9
|
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=20.19"
|
|
12
|
+
},
|
|
10
13
|
"scripts": {
|
|
11
|
-
"
|
|
14
|
+
"lint": "eslint . && prettier --check .",
|
|
15
|
+
"format": "prettier --write . && eslint --fix .",
|
|
16
|
+
"test": "node --test test/*.test.js",
|
|
17
|
+
"test:watch": "node --test --watch test/*.test.js",
|
|
18
|
+
"fixtures": "node tools/fixtures-from-paramsets.js"
|
|
12
19
|
},
|
|
13
20
|
"repository": {
|
|
14
21
|
"type": "git",
|
|
@@ -26,30 +33,32 @@
|
|
|
26
33
|
],
|
|
27
34
|
"author": "Sebastian Raff <hobbyquaker@gmail.com>",
|
|
28
35
|
"license": "MIT",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
29
39
|
"bugs": {
|
|
30
40
|
"url": "https://github.com/hobbyquaker/hm-simulator/issues"
|
|
31
41
|
},
|
|
32
42
|
"homepage": "https://github.com/hobbyquaker/hm-simulator#readme",
|
|
43
|
+
"files": [
|
|
44
|
+
"index.js",
|
|
45
|
+
"sim.js",
|
|
46
|
+
"sim.mjs",
|
|
47
|
+
"lib/",
|
|
48
|
+
"data/",
|
|
49
|
+
"behaviors/",
|
|
50
|
+
"README.md",
|
|
51
|
+
"LICENSE"
|
|
52
|
+
],
|
|
33
53
|
"dependencies": {
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"body-parser": "^1.18.3",
|
|
37
|
-
"express": "^4.16.4",
|
|
38
|
-
"homematic-xmlrpc": "^1.0.2",
|
|
39
|
-
"request": "^2.88.0",
|
|
40
|
-
"yalm": "^4.0.2"
|
|
54
|
+
"binrpc": "^4.2.0",
|
|
55
|
+
"homematic-xmlrpc": "^2.0.0"
|
|
41
56
|
},
|
|
42
57
|
"devDependencies": {
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
"rules": {
|
|
49
|
-
"no-cond-assign": "warn",
|
|
50
|
-
"no-case-declarations": "warn",
|
|
51
|
-
"no-constant-condition": "warn",
|
|
52
|
-
"no-unused-vars": "warn"
|
|
53
|
-
}
|
|
58
|
+
"@eslint/js": "^9.39.5",
|
|
59
|
+
"eslint": "^9.39.5",
|
|
60
|
+
"eslint-config-prettier": "^10.1.8",
|
|
61
|
+
"globals": "^16.5.0",
|
|
62
|
+
"prettier": "^3.9.6"
|
|
54
63
|
}
|
|
55
64
|
}
|