node-red-contrib-ccu 4.7.0 → 4.7.2
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/.mocharc.json +4 -0
- package/CHANGELOG.md +51 -0
- package/README.md +1 -1
- package/nodes/ccu-connection.html +1 -1
- package/nodes/ccu-connection.js +64 -43
- package/nodes/lib/cachefile.js +105 -0
- package/nodes/lib/lite.js +60 -6
- package/package.json +1 -1
package/.mocharc.json
ADDED
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,57 @@ Notable changes to node-red-contrib-ccu. Format follows
|
|
|
5
5
|
user-visible symptom and the cause, not the commit list (the release notes
|
|
6
6
|
append commits automatically).
|
|
7
7
|
|
|
8
|
+
## 4.7.2 (2026-10-29)
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **On openccu-lite the metadata file was written once per interface at every
|
|
13
|
+
start** (`metadata saved to …/ccu_<host>.json`, four lines within a second
|
|
14
|
+
on a system with four interfaces), although nothing had changed. The
|
|
15
|
+
system's stream hands the connection every interface's whole device list
|
|
16
|
+
when it connects, in the shape of a `newDevices` call, and the connection
|
|
17
|
+
saved whenever that list was not empty - on a CCU `newDevices` only ever
|
|
18
|
+
names devices the connection did not know. The file is now written only
|
|
19
|
+
when a device is new or its data changed, a burst of changes is one write
|
|
20
|
+
(and one log line), and the cache files (metadata, names, values) are
|
|
21
|
+
written atomically - a temporary file and a rename - so a power cut while
|
|
22
|
+
writing leaves the old file, not a truncated one.
|
|
23
|
+
- **On openccu-lite the start log's `Interfaces:` line listed ReGaHSS**, right
|
|
24
|
+
after the line saying the system has no ReGaHSS; it leaves it out now (the
|
|
25
|
+
mqtt node's per-interface counters as well). Log and dialog texts say "the
|
|
26
|
+
system" where they said "the box".
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- The README names RedMatic 9.15.1 as the version whose token makes lite-rpc
|
|
31
|
+
the default on the system (not 9.13).
|
|
32
|
+
- The release workflow's flow-library step no longer trusts the library's
|
|
33
|
+
"already at latest version" while its page still shows the previous
|
|
34
|
+
release; it asks again after 2, 5, 10 and 20 minutes and warns only after
|
|
35
|
+
that.
|
|
36
|
+
|
|
37
|
+
## 4.7.1 (2026-10-28)
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **On openccu-lite every write of a decimal value failed** - a dimmer's or a
|
|
42
|
+
blind's `LEVEL`, a thermostat's set point, an `ON_TIME`, through _value_,
|
|
43
|
+
_set value_, _rpc_ and the other nodes alike. A `FLOAT` datapoint is sent as
|
|
44
|
+
a typed double, which over XML-RPC is homematic-xmlrpc's
|
|
45
|
+
`{explicitDouble: n}`; the lite-rpc connection of 4.7.0 passed that object
|
|
46
|
+
on as it was, so the interface process got a struct where a number belongs
|
|
47
|
+
and refused it (`Failure` from rfd and hs485d,
|
|
48
|
+
`-5 Invalid parameter or value` from hmipserver). The lite-rpc connection
|
|
49
|
+
now sends lite-rpc's own typed double, `{double: n}` (or the plain number
|
|
50
|
+
on a system that does not announce it) - in `setValue`, in a paramset of
|
|
51
|
+
`putParamset` and in a multicall. The direct connection to a CCU is unchanged. Until this version
|
|
52
|
+
the workaround was _CCU_ in the connection node's _openccu-lite interfaces_.
|
|
53
|
+
- **Errors of the lite-rpc connection said less than they should:** a fault
|
|
54
|
+
of the interface process was logged as `fault -1 undefined` and reached the
|
|
55
|
+
flow as `OccuLiteError: Failure`, and a token without the needed rights did
|
|
56
|
+
not say which right it lacks. The fault now carries its code and text, and a
|
|
57
|
+
refused call names the missing scope.
|
|
58
|
+
|
|
8
59
|
## 4.7.0 (2026-10-27)
|
|
9
60
|
|
|
10
61
|
### Added
|
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ start on openccu-lite, the log says so, and a choice made in the node is never c
|
|
|
47
47
|
CCU-Jack keep their own connections.
|
|
48
48
|
|
|
49
49
|
- Node-RED **on the system** (RedMatic from its addon catalogue): lite-rpc with RedMatic's own token, which the system
|
|
50
|
-
mints for it from RedMatic's manifest (RedMatic 9.
|
|
50
|
+
mints for it from RedMatic's manifest (RedMatic 9.15.1 or later); the names come with the system's read-only token.
|
|
51
51
|
Nothing to configure. An older RedMatic has no such token and connects the CCU way, the log says so.
|
|
52
52
|
- Node-RED **elsewhere**: an API token of the system goes into the connection node's **openccu-lite token** field -
|
|
53
53
|
for lite-rpc it needs the `rpc:*` scope of what the flow does (`rpc:read` for events and reads, `rpc:operate` for
|
|
@@ -329,6 +329,6 @@
|
|
|
329
329
|
<div class="form-row">
|
|
330
330
|
<label for="node-config-input-metaPort"><i class="icon-tag"></i> openccu-lite port</label>
|
|
331
331
|
<input type="text" id="node-config-input-metaPort" placeholder="80 / 443">
|
|
332
|
-
<small>Only needed when the
|
|
332
|
+
<small>Only needed when the system's web server is not on the default port.</small>
|
|
333
333
|
</div>
|
|
334
334
|
</script>
|
package/nodes/ccu-connection.js
CHANGED
|
@@ -19,6 +19,7 @@ const {Rega} = require('homematic-rega'); // ES module - require(esm) needs Node
|
|
|
19
19
|
const metaProvider = require('./lib/metaprovider.js');
|
|
20
20
|
const {InitRetry, retryDelay, RETRY_INTERVAL} = require('./lib/initretry.js');
|
|
21
21
|
const lite = require('./lib/lite.js');
|
|
22
|
+
const {writeJsonAtomic, DebouncedSave} = require('./lib/cachefile.js');
|
|
22
23
|
const xmlrpc = require('homematic-xmlrpc');
|
|
23
24
|
const binrpc = require('binrpc');
|
|
24
25
|
|
|
@@ -781,6 +782,9 @@ module.exports = function (RED) {
|
|
|
781
782
|
RED.settings.userDir || path.join(__dirname, '..'),
|
|
782
783
|
'ccu_lite_' + this.host + '.json',
|
|
783
784
|
);
|
|
785
|
+
// task 24: the device lists of all interfaces arrive within a second at start -
|
|
786
|
+
// one write of the metadata file for the burst, flushed at close
|
|
787
|
+
this.metadataSave = new DebouncedSave(() => this.writeMetadata(), 1000);
|
|
784
788
|
|
|
785
789
|
this.loadMetadata();
|
|
786
790
|
this.loadRegadata();
|
|
@@ -1130,11 +1134,30 @@ module.exports = function (RED) {
|
|
|
1130
1134
|
* @returns {Promise<any>}
|
|
1131
1135
|
*/
|
|
1132
1136
|
saveMetadata() {
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1137
|
+
// task 24: a burst (every interface's device list at start) is one write and one line
|
|
1138
|
+
this.metadataSave.request();
|
|
1139
|
+
return Promise.resolve();
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/** writes the metadata file now (the debounce's write, and the flush at close) */
|
|
1143
|
+
writeMetadata() {
|
|
1144
|
+
this.writeCache(this.metadataFile, 'metadata', this.metadata);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* task 24: one of the cache files, written atomically; a failure is logged, never thrown
|
|
1149
|
+
* (the caches are a convenience, the connection works without them)
|
|
1150
|
+
* @param {string} file
|
|
1151
|
+
* @param {string} what for the log line
|
|
1152
|
+
* @param {*} data
|
|
1153
|
+
*/
|
|
1154
|
+
writeCache(file, what, data) {
|
|
1155
|
+
try {
|
|
1156
|
+
writeJsonAtomic(file, data);
|
|
1157
|
+
this.logger.info(what + ' saved to', file);
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
this.logger.error(what + ' not saved to ' + file + ': ' + error.message);
|
|
1160
|
+
}
|
|
1138
1161
|
}
|
|
1139
1162
|
|
|
1140
1163
|
/**
|
|
@@ -1142,25 +1165,19 @@ module.exports = function (RED) {
|
|
|
1142
1165
|
* @returns {Promise<any>}
|
|
1143
1166
|
*/
|
|
1144
1167
|
saveRegadata() {
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
groups: this.groups,
|
|
1157
|
-
sysvar: this.sysvar,
|
|
1158
|
-
program: this.program,
|
|
1159
|
-
}),
|
|
1160
|
-
);
|
|
1161
|
-
this.logger.info('regadata saved to', this.regadataFile);
|
|
1162
|
-
resolve();
|
|
1168
|
+
this.writeCache(this.regadataFile, 'regadata', {
|
|
1169
|
+
channelNames: this.channelNames,
|
|
1170
|
+
regaIdChannel: this.regaIdChannel,
|
|
1171
|
+
regaChannels: this.regaChannels,
|
|
1172
|
+
channelRooms: this.channelRooms,
|
|
1173
|
+
channelFunctions: this.channelFunctions,
|
|
1174
|
+
rooms: this.rooms,
|
|
1175
|
+
functions: this.functions,
|
|
1176
|
+
groups: this.groups,
|
|
1177
|
+
sysvar: this.sysvar,
|
|
1178
|
+
program: this.program,
|
|
1163
1179
|
});
|
|
1180
|
+
return Promise.resolve();
|
|
1164
1181
|
}
|
|
1165
1182
|
|
|
1166
1183
|
/**
|
|
@@ -1168,16 +1185,8 @@ module.exports = function (RED) {
|
|
|
1168
1185
|
* @returns {Promise<any>}
|
|
1169
1186
|
*/
|
|
1170
1187
|
saveValues() {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
this.valuesFile,
|
|
1174
|
-
JSON.stringify({
|
|
1175
|
-
values: this.values,
|
|
1176
|
-
}),
|
|
1177
|
-
);
|
|
1178
|
-
this.logger.info('values saved to', this.valuesFile);
|
|
1179
|
-
resolve();
|
|
1180
|
-
});
|
|
1188
|
+
this.writeCache(this.valuesFile, 'values', {values: this.values});
|
|
1189
|
+
return Promise.resolve();
|
|
1181
1190
|
}
|
|
1182
1191
|
|
|
1183
1192
|
/**
|
|
@@ -1353,6 +1362,7 @@ module.exports = function (RED) {
|
|
|
1353
1362
|
Object.keys(this.liveness).forEach((iface) => this.stopLiveness(iface));
|
|
1354
1363
|
clearTimeout(this.valuesPruneTimer);
|
|
1355
1364
|
|
|
1365
|
+
this.metadataSave.flush();
|
|
1356
1366
|
this.saveRegadata();
|
|
1357
1367
|
this.saveValues();
|
|
1358
1368
|
|
|
@@ -1491,7 +1501,7 @@ module.exports = function (RED) {
|
|
|
1491
1501
|
const line =
|
|
1492
1502
|
'meta api detection inconclusive (' +
|
|
1493
1503
|
outcome.reason +
|
|
1494
|
-
') - names come from the ReGaHSS until the
|
|
1504
|
+
') - names come from the ReGaHSS until the system answers, detecting again in ' +
|
|
1495
1505
|
delay / 1000 +
|
|
1496
1506
|
' s';
|
|
1497
1507
|
if (this.metaRedetectAttempt === 1) {
|
|
@@ -1568,6 +1578,8 @@ module.exports = function (RED) {
|
|
|
1568
1578
|
delete this.ifaceStatus.ReGaHSS;
|
|
1569
1579
|
delete this.ifaceWaiting.ReGaHSS;
|
|
1570
1580
|
this.ifaceStatus[META_IFACE] = null;
|
|
1581
|
+
// task 25: the system has no ReGaHSS - the "Interfaces:" line and the counters leave it out
|
|
1582
|
+
this.enabledIfaces = this.enabledIfaces.filter((iface) => iface !== 'ReGaHSS');
|
|
1571
1583
|
|
|
1572
1584
|
this.logger.info(
|
|
1573
1585
|
'openccu-lite detected (' +
|
|
@@ -1577,12 +1589,12 @@ module.exports = function (RED) {
|
|
|
1577
1589
|
') - names, rooms and functions come from its metadata api',
|
|
1578
1590
|
);
|
|
1579
1591
|
this.logger.info(
|
|
1580
|
-
'this
|
|
1592
|
+
'this system has no ReGaHSS: system variables, programs and HM-Script are not available (sysvar, program, poll and script nodes stay idle)',
|
|
1581
1593
|
);
|
|
1582
1594
|
|
|
1583
1595
|
if (info.version > 1) {
|
|
1584
1596
|
this.logger.warn(
|
|
1585
|
-
'the
|
|
1597
|
+
'the system speaks metadata api version ' +
|
|
1586
1598
|
info.version +
|
|
1587
1599
|
', this version of node-red-contrib-ccu implements version 1',
|
|
1588
1600
|
);
|
|
@@ -1590,7 +1602,7 @@ module.exports = function (RED) {
|
|
|
1590
1602
|
|
|
1591
1603
|
if (!this.metaToken) {
|
|
1592
1604
|
this.logger.warn(
|
|
1593
|
-
'no metadata api token: create one on the
|
|
1605
|
+
'no metadata api token: create one on the system (Users page) and enter it in the connection node, or run on the system where ' +
|
|
1594
1606
|
metaProvider.LOCAL_TOKEN_FILE +
|
|
1595
1607
|
' is readable. Without it the nodes work with addresses only.',
|
|
1596
1608
|
);
|
|
@@ -1631,10 +1643,13 @@ module.exports = function (RED) {
|
|
|
1631
1643
|
delete this.ifaceStatus[META_IFACE];
|
|
1632
1644
|
if (this.ifaceTypes.ReGaHSS.enabled) {
|
|
1633
1645
|
this.ifaceStatus.ReGaHSS = null;
|
|
1646
|
+
if (!this.enabledIfaces.includes('ReGaHSS')) {
|
|
1647
|
+
this.enabledIfaces.unshift('ReGaHSS');
|
|
1648
|
+
}
|
|
1634
1649
|
}
|
|
1635
1650
|
|
|
1636
1651
|
this.pushStatus();
|
|
1637
|
-
this.logger.warn('this
|
|
1652
|
+
this.logger.warn('this system no longer answers as openccu-lite - detecting again');
|
|
1638
1653
|
this.detectMetaOutcome().then((outcome) => {
|
|
1639
1654
|
if (outcome.info) {
|
|
1640
1655
|
return this.startMeta(outcome.info);
|
|
@@ -1717,7 +1732,7 @@ module.exports = function (RED) {
|
|
|
1717
1732
|
* @returns {Error}
|
|
1718
1733
|
*/
|
|
1719
1734
|
regaMissingError(feature) {
|
|
1720
|
-
return new Error(feature + ' are not available on this
|
|
1735
|
+
return new Error(feature + ' are not available on this system (openccu-lite has no ReGaHSS)');
|
|
1721
1736
|
}
|
|
1722
1737
|
|
|
1723
1738
|
/**
|
|
@@ -3456,9 +3471,12 @@ module.exports = function (RED) {
|
|
|
3456
3471
|
}
|
|
3457
3472
|
|
|
3458
3473
|
/**
|
|
3459
|
-
*
|
|
3474
|
+
* Takes a device description into the device table.
|
|
3460
3475
|
* @param iface
|
|
3461
3476
|
* @param device
|
|
3477
|
+
* @returns {boolean} whether the table changed: the device is new, or its data changed
|
|
3478
|
+
* (a description equal to the cached one, as the lite-rpc stream's snapshot sends at
|
|
3479
|
+
* every connect, changes nothing - task 24)
|
|
3462
3480
|
*/
|
|
3463
3481
|
newDevice(iface, device) {
|
|
3464
3482
|
if (!this.metadata.devices[iface]) {
|
|
@@ -3497,7 +3515,8 @@ module.exports = function (RED) {
|
|
|
3497
3515
|
this.logger.debug('newDevice', iface, device.ADDRESS);
|
|
3498
3516
|
}
|
|
3499
3517
|
|
|
3500
|
-
|
|
3518
|
+
const changed = !known || deviceChanged(known, device);
|
|
3519
|
+
if (changed) {
|
|
3501
3520
|
// B-36: a device added anew (re-paired, or its data changed) is asked again
|
|
3502
3521
|
this.forgetParamsetFailures(iface, device.ADDRESS);
|
|
3503
3522
|
}
|
|
@@ -3522,6 +3541,7 @@ module.exports = function (RED) {
|
|
|
3522
3541
|
}
|
|
3523
3542
|
|
|
3524
3543
|
this.paramsetQueuePush(iface, device);
|
|
3544
|
+
return changed;
|
|
3525
3545
|
}
|
|
3526
3546
|
|
|
3527
3547
|
/**
|
|
@@ -3860,8 +3880,9 @@ module.exports = function (RED) {
|
|
|
3860
3880
|
if (iface && Array.isArray(devices)) {
|
|
3861
3881
|
devices.forEach((device) => {
|
|
3862
3882
|
if (device && typeof device.ADDRESS === 'string' && device.TYPE) {
|
|
3863
|
-
this.newDevice(iface, device)
|
|
3864
|
-
|
|
3883
|
+
if (this.newDevice(iface, device)) {
|
|
3884
|
+
changed = true;
|
|
3885
|
+
}
|
|
3865
3886
|
} else {
|
|
3866
3887
|
this.logger.warn(
|
|
3867
3888
|
'newDevices',
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The connection's cache files (metadata, regadata, values) on disk.
|
|
3
|
+
*
|
|
4
|
+
* Written atomically: the JSON goes to a temporary file beside the target and is renamed over it,
|
|
5
|
+
* so a crash or a power cut while writing leaves the old file, never a truncated one (the files
|
|
6
|
+
* are read with JSON.parse at the next start, and a half-written one would throw the cache away).
|
|
7
|
+
*
|
|
8
|
+
* A burst of changes is written once: `DebouncedSave` collects the requests of a short window into
|
|
9
|
+
* one write - at start every interface reports its device list within the same second, and before
|
|
10
|
+
* there was one full rewrite and one log line per interface.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Writes `data` as JSON to `file` by way of a temporary file and a rename; an existing file's mode
|
|
18
|
+
* is kept.
|
|
19
|
+
* @param {string} file
|
|
20
|
+
* @param {*} data
|
|
21
|
+
* @param {string|number} [indent] JSON.stringify's third argument
|
|
22
|
+
*/
|
|
23
|
+
function writeJsonAtomic(file, data, indent) {
|
|
24
|
+
const temporary = path.join(
|
|
25
|
+
path.dirname(file),
|
|
26
|
+
'.' + path.basename(file) + '.' + process.pid + '.' + Date.now().toString(36) + '.tmp',
|
|
27
|
+
);
|
|
28
|
+
try {
|
|
29
|
+
fs.writeFileSync(temporary, JSON.stringify(data, null, indent));
|
|
30
|
+
// the rename replaces the inode: an existing file's mode is kept (0600 on a system that
|
|
31
|
+
// made it so), a new one gets the umask's
|
|
32
|
+
let mode;
|
|
33
|
+
try {
|
|
34
|
+
mode = fs.statSync(file).mode & 0o777;
|
|
35
|
+
} catch {}
|
|
36
|
+
|
|
37
|
+
if (mode !== undefined) {
|
|
38
|
+
fs.chmodSync(temporary, mode);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fs.renameSync(temporary, file);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
try {
|
|
44
|
+
fs.unlinkSync(temporary);
|
|
45
|
+
} catch {}
|
|
46
|
+
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class DebouncedSave {
|
|
52
|
+
/**
|
|
53
|
+
* @param {Function} write does the write; called once per burst
|
|
54
|
+
* @param {number} [delay] the window, ms (default 1000)
|
|
55
|
+
*/
|
|
56
|
+
constructor(write, delay = 1000) {
|
|
57
|
+
this.write = write;
|
|
58
|
+
this.delay = delay;
|
|
59
|
+
this.timer = null;
|
|
60
|
+
this.pending = false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** asks for a write; one write happens `delay` ms after the first request of a burst */
|
|
64
|
+
request() {
|
|
65
|
+
this.pending = true;
|
|
66
|
+
if (this.timer) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.timer = setTimeout(() => {
|
|
71
|
+
this.timer = null;
|
|
72
|
+
this.flush();
|
|
73
|
+
}, this.delay);
|
|
74
|
+
if (typeof this.timer.unref === 'function') {
|
|
75
|
+
this.timer.unref();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** writes now when a request is pending (close, redeploy) */
|
|
80
|
+
flush() {
|
|
81
|
+
if (this.timer) {
|
|
82
|
+
clearTimeout(this.timer);
|
|
83
|
+
this.timer = null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!this.pending) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.pending = false;
|
|
91
|
+
this.write();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** drops a pending request without writing */
|
|
95
|
+
cancel() {
|
|
96
|
+
if (this.timer) {
|
|
97
|
+
clearTimeout(this.timer);
|
|
98
|
+
this.timer = null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.pending = false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {writeJsonAtomic, DebouncedSave};
|
package/nodes/lib/lite.js
CHANGED
|
@@ -80,7 +80,26 @@ function readAddonToken(file = module.exports.ADDON_TOKEN_FILE) {
|
|
|
80
80
|
* @returns {boolean}
|
|
81
81
|
*/
|
|
82
82
|
function isFault(error) {
|
|
83
|
-
return
|
|
83
|
+
return kindOf(error) === 'rpc-fault' && error.faultCode !== undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The client's error kind: occulite-client's OccuLiteError carries it as `code` (rpc-fault,
|
|
88
|
+
* forbidden, timeout ...); `kind` is ours, on an error translated before. A Node system error's
|
|
89
|
+
* `code` (ECONNREFUSED ...) is not a kind.
|
|
90
|
+
* @param {*} error
|
|
91
|
+
* @returns {string|undefined}
|
|
92
|
+
*/
|
|
93
|
+
function kindOf(error) {
|
|
94
|
+
if (!error) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (error.kind) {
|
|
99
|
+
return error.kind;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return error.name === 'OccuLiteError' && typeof error.code === 'string' ? error.code : undefined;
|
|
84
103
|
}
|
|
85
104
|
|
|
86
105
|
/**
|
|
@@ -97,7 +116,8 @@ function translateError(error) {
|
|
|
97
116
|
return fault;
|
|
98
117
|
}
|
|
99
118
|
|
|
100
|
-
|
|
119
|
+
const kind = kindOf(error);
|
|
120
|
+
if (kind === 'forbidden') {
|
|
101
121
|
const refused = new Error(
|
|
102
122
|
'the openccu-lite token may not call this method' + (error.scope ? ' (needs ' + error.scope + ')' : ''),
|
|
103
123
|
);
|
|
@@ -106,15 +126,47 @@ function translateError(error) {
|
|
|
106
126
|
return refused;
|
|
107
127
|
}
|
|
108
128
|
|
|
109
|
-
if (
|
|
110
|
-
const translated = new Error(error.message ||
|
|
111
|
-
translated.kind =
|
|
129
|
+
if (kind) {
|
|
130
|
+
const translated = new Error(error.message || kind);
|
|
131
|
+
translated.kind = kind;
|
|
112
132
|
return translated;
|
|
113
133
|
}
|
|
114
134
|
|
|
115
135
|
return error instanceof Error ? error : new Error(String(error));
|
|
116
136
|
}
|
|
117
137
|
|
|
138
|
+
/**
|
|
139
|
+
* B-41: cast.js marks a FLOAT as homematic-xmlrpc's `{explicitDouble: n}` (the XML-RPC client's
|
|
140
|
+
* `<double>`). lite-rpc's JSON path does not know that marker and would hand the process a struct
|
|
141
|
+
* where a double belongs - hs485d and hmipserver refuse it. Its own marker is `{double: n}`
|
|
142
|
+
* (capability `json_double`); a system without it gets the plain number. Walks the whole
|
|
143
|
+
* parameter list: the top level, structs and paramsets, a multicall's calls.
|
|
144
|
+
* @param {*} value
|
|
145
|
+
* @param {boolean} jsonDouble whether the system announces `json_double`
|
|
146
|
+
* @returns {*} the value for lite-rpc
|
|
147
|
+
*/
|
|
148
|
+
function liteValue(value, jsonDouble) {
|
|
149
|
+
if (Array.isArray(value)) {
|
|
150
|
+
return value.map((item) => liteValue(item, jsonDouble));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
|
|
154
|
+
const keys = Object.keys(value);
|
|
155
|
+
if (keys.length === 1 && keys[0] === 'explicitDouble') {
|
|
156
|
+
const number = Number(value.explicitDouble);
|
|
157
|
+
return jsonDouble ? {double: number} : number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const out = {};
|
|
161
|
+
keys.forEach((key) => {
|
|
162
|
+
out[key] = liteValue(value[key], jsonDouble);
|
|
163
|
+
});
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
|
|
118
170
|
const RPC_TIERS = ['rpc:operate', 'rpc:configure', 'rpc:admin'];
|
|
119
171
|
|
|
120
172
|
/**
|
|
@@ -263,7 +315,8 @@ class LiteConnection {
|
|
|
263
315
|
return Promise.reject(new Error('lite-rpc client not started'));
|
|
264
316
|
}
|
|
265
317
|
|
|
266
|
-
|
|
318
|
+
const jsonDouble = Boolean(this.box.capabilities && this.box.capabilities.json_double);
|
|
319
|
+
return this.box.call(iface, method, liteValue(parameters || [], jsonDouble)).then(
|
|
267
320
|
(result) => (result === null || result === undefined ? '' : result),
|
|
268
321
|
(error) => {
|
|
269
322
|
throw translateError(error);
|
|
@@ -302,6 +355,7 @@ module.exports = {
|
|
|
302
355
|
ADDON_TOKEN_FILE,
|
|
303
356
|
CALLBACK_METHODS,
|
|
304
357
|
readAddonToken,
|
|
358
|
+
liteValue,
|
|
305
359
|
tierAwareTransport,
|
|
306
360
|
translateError,
|
|
307
361
|
isFault,
|