node-red-contrib-ccu 4.7.0 → 4.7.1

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/CHANGELOG.md CHANGED
@@ -5,6 +5,28 @@ 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.1 (2026-10-28)
9
+
10
+ ### Fixed
11
+
12
+ - **On openccu-lite every write of a decimal value failed** - a dimmer's or a
13
+ blind's `LEVEL`, a thermostat's set point, an `ON_TIME`, through _value_,
14
+ _set value_, _rpc_ and the other nodes alike. A `FLOAT` datapoint is sent as
15
+ a typed double, which over XML-RPC is homematic-xmlrpc's
16
+ `{explicitDouble: n}`; the lite-rpc connection of 4.7.0 passed that object
17
+ on as it was, so the interface process got a struct where a number belongs
18
+ and refused it (`Failure` from rfd and hs485d,
19
+ `-5 Invalid parameter or value` from hmipserver). The lite-rpc connection
20
+ now sends lite-rpc's own typed double, `{double: n}` (or the plain number
21
+ on a system that does not announce it) - in `setValue`, in a paramset of
22
+ `putParamset` and in a multicall. The direct connection to a CCU is unchanged. Until this version
23
+ the workaround was _CCU_ in the connection node's _openccu-lite interfaces_.
24
+ - **Errors of the lite-rpc connection said less than they should:** a fault
25
+ of the interface process was logged as `fault -1 undefined` and reached the
26
+ flow as `OccuLiteError: Failure`, and a token without the needed rights did
27
+ not say which right it lacks. The fault now carries its code and text, and a
28
+ refused call names the missing scope.
29
+
8
30
  ## 4.7.0 (2026-10-27)
9
31
 
10
32
  ### Added
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 Boolean(error) && error.kind === 'rpc-fault' && error.faultCode !== undefined;
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
- if (error && error.kind === 'forbidden') {
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 (error && error.kind) {
110
- const translated = new Error(error.message || error.kind);
111
- translated.kind = error.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
- return this.box.call(iface, method, parameters || []).then(
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-ccu",
3
- "version": "4.7.0",
3
+ "version": "4.7.1",
4
4
  "license": "MIT",
5
5
  "main": "none",
6
6
  "keywords": [