node-red-contrib-ccu 4.6.2 → 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.
@@ -5,8 +5,13 @@
5
5
  * addon started before the interfaces, a daemon restart) used to leave the
6
6
  * interface without events: rpcCheckInit only runs with cached devices and
7
7
  * then waits pingTimeout (600 s for HmIP-RF). Now a failed init schedules its
8
- * own retry, 1 s, 2 s, 4 s, 8 s and then every 15 s (task 12), until it
9
- * succeeds or the node is closed.
8
+ * own retry (task 12) until it succeeds or the node is closed: every 3 s for
9
+ * the first 90 s after the interface was first missed (the start, or the
10
+ * moment a connected process went away), then every 15 s (task 17 - the
11
+ * 1/2/4/8/15 s steps of before hit hmipserver's ready moment, ~50-55 s after
12
+ * an openccu-lite boot, only at their 61 s tick). When one interface appears
13
+ * the others that are waiting are tried at once (poke): rfd and hmipserver
14
+ * come up close together.
10
15
  *
11
16
  * While the process is unreachable the interface is "waiting": one line, then
12
17
  * debug. Before the interface was ever connected, a refused connection is
@@ -17,20 +22,22 @@
17
22
  * distinct fault.
18
23
  */
19
24
 
20
- const RETRY_DELAYS = [1000, 2000, 4000, 8000];
25
+ const RETRY_INTERVAL = 3000;
26
+ const RETRY_WINDOW = 90000;
21
27
  const RETRY_MAX_DELAY = 15000;
22
28
 
23
29
  /**
24
- * The delay before the next attempt.
25
- * @param {number} failures failed attempts so far (1 after the first failure)
26
- * @returns {number} milliseconds
30
+ * The delay before the next attempt (task 17).
31
+ * @param {number} elapsed milliseconds since the interface was first missed
32
+ * @returns {number} milliseconds: 3 s within the first 90 s, then 15 s
27
33
  */
28
- function retryDelay(failures) {
29
- if (failures >= 1 && failures <= RETRY_DELAYS.length) {
30
- return RETRY_DELAYS[failures - 1];
31
- }
34
+ function retryDelay(elapsed) {
35
+ return elapsed < RETRY_WINDOW ? RETRY_INTERVAL : RETRY_MAX_DELAY;
36
+ }
32
37
 
33
- return RETRY_MAX_DELAY;
38
+ /** @param {number} ms @returns {string} seconds with one decimal, e.g. "54.0 s" */
39
+ function seconds(ms) {
40
+ return (Math.max(0, ms) / 1000).toFixed(1) + ' s';
34
41
  }
35
42
 
36
43
  /**
@@ -92,14 +99,19 @@ class InitRetry {
92
99
  * @param {object} options.logger {debug, info, warn, error}
93
100
  * @param {function(string)} options.onState 'waiting' | 'failed' | 'connected'
94
101
  * @param {object} [options.timers] {setTimeout, clearTimeout}, for tests
102
+ * @param {function(): number} [options.now] the clock, for tests
103
+ * @param {number} [options.startedAt] when the program (the connection node) started, for the log line
95
104
  */
96
- constructor({iface, attempt, logger, onState, timers}) {
105
+ constructor({iface, attempt, logger, onState, timers, now, startedAt}) {
97
106
  this.iface = iface;
98
107
  this.attempt = attempt;
99
108
  this.logger = logger;
100
109
  this.onState = onState || (() => {});
101
110
  this.timers = timers || {setTimeout, clearTimeout};
111
+ this.now = now || Date.now;
112
+ this.startedAt = startedAt === undefined ? this.now() : startedAt;
102
113
  this.timer = null;
114
+ this.running = false;
103
115
  this.stopped = false;
104
116
  this.everConnected = false;
105
117
  this.reset();
@@ -109,6 +121,8 @@ class InitRetry {
109
121
  this.failures = 0;
110
122
  this.warned = false;
111
123
  this.faults = new Set();
124
+ // task 17: when the process was first missed - the 90 s of 3 s steps count from here
125
+ this.missedAt = null;
112
126
  }
113
127
 
114
128
  /** true while a retry is scheduled */
@@ -125,8 +139,12 @@ class InitRetry {
125
139
  return;
126
140
  }
127
141
 
142
+ if (this.failures === 0) {
143
+ this.missedAt = this.now();
144
+ }
145
+
128
146
  this.failures += 1;
129
- const delay = retryDelay(this.failures);
147
+ const delay = retryDelay(this.now() - this.missedAt);
130
148
  const seconds = delay / 1000;
131
149
  const reason = describeError(error);
132
150
  if (isFault(error)) {
@@ -177,14 +195,38 @@ class InitRetry {
177
195
  return Promise.resolve();
178
196
  }
179
197
 
198
+ this.running = true;
180
199
  return Promise.resolve()
181
200
  .then(() => this.attempt())
182
201
  .then(
183
- () => this.succeeded(),
184
- (error) => this.failed(error),
202
+ () => {
203
+ this.running = false;
204
+ this.succeeded();
205
+ },
206
+ (error) => {
207
+ this.running = false;
208
+ this.failed(error);
209
+ },
185
210
  );
186
211
  }
187
212
 
213
+ /**
214
+ * task 17: another interface just appeared - try this one now instead of at its timer
215
+ * (rfd and hmipserver come up close together). Nothing while an attempt is running or
216
+ * none is scheduled.
217
+ * @returns {boolean} whether an attempt was started
218
+ */
219
+ poke() {
220
+ if (this.stopped || this.running || this.timer === null) {
221
+ return false;
222
+ }
223
+
224
+ this.clear();
225
+ this.logger.debug(this.iface + ': another interface appeared, trying now');
226
+ this.run();
227
+ return true;
228
+ }
229
+
188
230
  /** an init attempt succeeded */
189
231
  succeeded() {
190
232
  if (this.stopped) {
@@ -193,7 +235,11 @@ class InitRetry {
193
235
 
194
236
  this.clear();
195
237
  if (this.failures > 0) {
196
- this.logger.info(this.iface + ' connected after ' + (this.failures + 1) + ' attempts');
238
+ // task 17: one line with the time, so a log shows how close the start got to the process's ready moment
239
+ const since = this.everConnected
240
+ ? seconds(this.now() - this.missedAt) + ' after it went missing'
241
+ : seconds(this.now() - this.startedAt) + ' after the start';
242
+ this.logger.info(this.iface + ' connected after ' + (this.failures + 1) + ' attempts, ' + since);
197
243
  }
198
244
 
199
245
  this.everConnected = true;
@@ -215,4 +261,13 @@ class InitRetry {
215
261
  }
216
262
  }
217
263
 
218
- module.exports = {InitRetry, retryDelay, isFault, isRefused, describeError, RETRY_DELAYS, RETRY_MAX_DELAY};
264
+ module.exports = {
265
+ InitRetry,
266
+ retryDelay,
267
+ isFault,
268
+ isRefused,
269
+ describeError,
270
+ RETRY_INTERVAL,
271
+ RETRY_WINDOW,
272
+ RETRY_MAX_DELAY,
273
+ };
@@ -0,0 +1,363 @@
1
+ /*
2
+ * task 14: the interfaces of an openccu-lite system through its lite-rpc, with occulite-client,
3
+ * instead of XML-RPC/BIN-RPC clients and callback servers of our own.
4
+ *
5
+ * One client per connection node: the requests go to /api/rpc/v1 with the credential, the events
6
+ * come from the stream the client opens (resumed after a break, re-read after a restart of the
7
+ * system), and `callbackEvents` turns them into the very calls a callback server would make -
8
+ * event, system.multicall, newDevices, deleteDevices, updateDevice, replaceDevice, readdedDevice
9
+ * with the same argument lists - so the connection's rpcMethods table feeds from it unchanged.
10
+ * No init, no ping, no callback ports for these interfaces. CUxD and CCU-Jack are not carried by
11
+ * lite-rpc and keep their own clients.
12
+ *
13
+ * The credential: the openccu-lite token of the connection node, else the addon's own token the
14
+ * system mints for RedMatic from its manifest (/run/occulite/addon-tokens/redmatic.api). The local
15
+ * token (meta:read alone) is no credential for lite-rpc.
16
+ *
17
+ * occulite-client is ESM; it is loaded here, lazily, so a CCU user never loads it and a Node too
18
+ * old for require(esm) gets a message instead of a crash (B-32's lesson).
19
+ */
20
+
21
+ const fs = require('fs');
22
+
23
+ const LITE_INTERFACES = ['BidCos-RF', 'BidCos-Wired', 'HmIP-RF', 'VirtualDevices'];
24
+ const ADDON_TOKEN_FILE = '/run/occulite/addon-tokens/redmatic.api';
25
+
26
+ /** the stream's events as the callback server's methods, see callbackEvents() */
27
+ const CALLBACK_METHODS = [
28
+ 'event',
29
+ 'system.multicall',
30
+ 'newDevices',
31
+ 'deleteDevices',
32
+ 'updateDevice',
33
+ 'replaceDevice',
34
+ 'readdedDevice',
35
+ ];
36
+
37
+ let modules = null;
38
+
39
+ /**
40
+ * occulite-client, loaded once. Throws with a readable message where it cannot be loaded.
41
+ * @returns {{OccuLite: Function, callbackEvents: Function, nodeTransport: Function}}
42
+ */
43
+ function load() {
44
+ if (!modules) {
45
+ try {
46
+ const {OccuLite} = require('occulite-client');
47
+ const {callbackEvents} = require('occulite-client/callbacks');
48
+ const {nodeTransport} = require('occulite-client/node');
49
+ modules = {OccuLite, callbackEvents, nodeTransport};
50
+ } catch (error) {
51
+ throw new Error(
52
+ 'occulite-client cannot be loaded (' +
53
+ (error.message || error) +
54
+ '): lite-rpc needs Node.js 20.19, 22.12 or newer; the direct connection is used instead',
55
+ );
56
+ }
57
+ }
58
+
59
+ return modules;
60
+ }
61
+
62
+ /**
63
+ * The addon's own token on an openccu-lite system (RedMatic's, minted from its manifest), or
64
+ * undefined where there is none.
65
+ * @param {string} [file] the token file, for tests
66
+ * @returns {string|undefined}
67
+ */
68
+ function readAddonToken(file = module.exports.ADDON_TOKEN_FILE) {
69
+ try {
70
+ const token = fs.readFileSync(file, 'utf8').trim();
71
+ return token === '' ? undefined : token;
72
+ } catch {
73
+ return undefined;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Whether an error of the client is an RPC fault of the interface process (the process answered).
79
+ * @param {*} error
80
+ * @returns {boolean}
81
+ */
82
+ function isFault(error) {
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;
103
+ }
104
+
105
+ /**
106
+ * The client's error as the connection reports it: a fault keeps its code and string, a refused
107
+ * scope says which, everything else its kind.
108
+ * @param {*} error
109
+ * @returns {Error}
110
+ */
111
+ function translateError(error) {
112
+ if (isFault(error)) {
113
+ const fault = new Error(error.faultString || error.message);
114
+ fault.faultCode = error.faultCode;
115
+ fault.faultString = error.faultString || error.message;
116
+ return fault;
117
+ }
118
+
119
+ const kind = kindOf(error);
120
+ if (kind === 'forbidden') {
121
+ const refused = new Error(
122
+ 'the openccu-lite token may not call this method' + (error.scope ? ' (needs ' + error.scope + ')' : ''),
123
+ );
124
+ refused.kind = 'forbidden';
125
+ refused.scope = error.scope;
126
+ return refused;
127
+ }
128
+
129
+ if (kind) {
130
+ const translated = new Error(error.message || kind);
131
+ translated.kind = kind;
132
+ return translated;
133
+ }
134
+
135
+ return error instanceof Error ? error : new Error(String(error));
136
+ }
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
+
170
+ const RPC_TIERS = ['rpc:operate', 'rpc:configure', 'rpc:admin'];
171
+
172
+ /**
173
+ * occulite-client B-3: the client decides "names only" from the literal `rpc:read` in
174
+ * GET /api/auth/v1/state, but a token made with a higher tier (rpc:admin, say) carries that tier
175
+ * alone - the system includes the lower ones. The transport spells `rpc:read` out for the client
176
+ * where a tier implies it. Goes once the package fixes it.
177
+ * @param {object} transport occulite-client's Transport
178
+ * @returns {object} the same transport
179
+ */
180
+ function tierAwareTransport(transport) {
181
+ return {
182
+ request: (method, url, headers, body, signal) =>
183
+ transport.request(method, url, headers, body, signal).then((response) => {
184
+ if (response.status !== 200 || !/\/api\/auth\/v1\/state(\?|$)/.test(String(url))) {
185
+ return response;
186
+ }
187
+
188
+ let parsed;
189
+ try {
190
+ parsed = JSON.parse(response.body);
191
+ } catch {
192
+ return response;
193
+ }
194
+
195
+ const scopes = Array.isArray(parsed && parsed.scopes) ? parsed.scopes : [];
196
+ if (scopes.includes('rpc:read') || !scopes.some((scope) => RPC_TIERS.includes(scope))) {
197
+ return response;
198
+ }
199
+
200
+ return {...response, body: JSON.stringify({...parsed, scopes: [...scopes, 'rpc:read']})};
201
+ }),
202
+ stream: (url, headers, signal) => transport.stream(url, headers, signal),
203
+ };
204
+ }
205
+
206
+ class LiteConnection {
207
+ /**
208
+ * @param {object} options
209
+ * @param {string} options.url the system, scheme and authority
210
+ * @param {string} options.token the credential
211
+ * @param {boolean} [options.insecure] accept any certificate
212
+ * @param {string[]} options.interfaces the interfaces to follow (the enabled ones lite-rpc carries)
213
+ * @param {function(string): string} options.initId the init id the connection's callback table expects per interface
214
+ * @param {function(string, *, Array, Function)} options.onCallback (method, err, params, answer): a callback server's call
215
+ * @param {function(string, string)} options.onInterface (iface, state): up | down | restarted | added | removed
216
+ * @param {function(string)} options.onStatus the client's state: connecting | ready | degraded | offline | closed
217
+ * @param {function(Map)} [options.onValues] box.values once ready (the state store's picture)
218
+ * @param {object} options.logger {debug, info, warn, error}
219
+ * @param {number} [options.timeout] per request, ms
220
+ * @param {Function} [options.createClient] for tests, in place of new OccuLite()
221
+ */
222
+ constructor(options) {
223
+ this.options = options;
224
+ this.interfaces = [...options.interfaces];
225
+ this.logger = options.logger;
226
+ this.box = null;
227
+ this.server = null;
228
+ this.closed = false;
229
+ this.ready = false;
230
+ }
231
+
232
+ /**
233
+ * @param {string} iface
234
+ * @returns {boolean} whether this interface goes through lite-rpc
235
+ */
236
+ has(iface) {
237
+ return this.interfaces.includes(iface);
238
+ }
239
+
240
+ /**
241
+ * Opens the client. Resolves once the first picture is loaded, rejects with the client's
242
+ * reason (unauthenticated, forbidden, unreachable, timeout ...); the stream keeps trying by
243
+ * itself afterwards.
244
+ * @returns {Promise<void>}
245
+ */
246
+ start() {
247
+ const {OccuLite, callbackEvents, nodeTransport} = load();
248
+ const debug = (message) => this.logger.debug('occulite-client: ' + message);
249
+ const clientOptions = {
250
+ url: this.options.url,
251
+ auth: {token: this.options.token},
252
+ transport: tierAwareTransport(nodeTransport(this.options.insecure ? {insecure: true} : {})),
253
+ interfaces: this.interfaces,
254
+ values: true,
255
+ meta: false,
256
+ devices: true,
257
+ timeout: this.options.timeout || 30000,
258
+ log: {debug, info: debug, warn: debug, error: debug},
259
+ };
260
+ const box = this.options.createClient ? this.options.createClient(clientOptions) : new OccuLite(clientOptions);
261
+ this.box = box;
262
+ box.on('status', (event) => {
263
+ if (this.closed || this.box !== box) {
264
+ return;
265
+ }
266
+
267
+ const state = event && event.state ? event.state : event;
268
+ this.ready = state === 'ready';
269
+ this.options.onStatus(state);
270
+ });
271
+ box.on('interface', (event) => {
272
+ if (!this.closed && this.box === box) {
273
+ this.options.onInterface(event.interface, event.state);
274
+ }
275
+ });
276
+ box.on('resync', () => {
277
+ if (!this.closed && this.box === box && this.options.onValues) {
278
+ this.options.onValues(box.values);
279
+ }
280
+ });
281
+ // the stream's events as a callback server's calls, into the connection's own table
282
+ this.server = callbackEvents(box, {initId: this.options.initId});
283
+ CALLBACK_METHODS.forEach((method) => {
284
+ this.server.on(method, (err, parameters, callback) => {
285
+ if (this.closed || this.box !== box) {
286
+ callback(null, '');
287
+ return;
288
+ }
289
+
290
+ this.options.onCallback(method, err, parameters, callback);
291
+ });
292
+ });
293
+
294
+ return box.ready().then(() => {
295
+ if (this.closed || this.box !== box) {
296
+ return;
297
+ }
298
+
299
+ this.ready = true;
300
+ if (this.options.onValues) {
301
+ this.options.onValues(box.values);
302
+ }
303
+ });
304
+ }
305
+
306
+ /**
307
+ * Any method of an interface process through lite-rpc; the credential's tier decides.
308
+ * @param {string} iface
309
+ * @param {string} method
310
+ * @param {Array} parameters
311
+ * @returns {Promise<*>}
312
+ */
313
+ call(iface, method, parameters) {
314
+ if (!this.box) {
315
+ return Promise.reject(new Error('lite-rpc client not started'));
316
+ }
317
+
318
+ const jsonDouble = Boolean(this.box.capabilities && this.box.capabilities.json_double);
319
+ return this.box.call(iface, method, liteValue(parameters || [], jsonDouble)).then(
320
+ (result) => (result === null || result === undefined ? '' : result),
321
+ (error) => {
322
+ throw translateError(error);
323
+ },
324
+ );
325
+ }
326
+
327
+ /**
328
+ * Whether the system says this interface runs (the client's live list).
329
+ * @param {string} iface
330
+ * @returns {boolean|undefined} undefined while the list is not known
331
+ */
332
+ running(iface) {
333
+ if (!this.box || !Array.isArray(this.box.interfaces)) {
334
+ return undefined;
335
+ }
336
+
337
+ const entry = this.box.interfaces.find((info) => info.name === iface);
338
+ return entry ? entry.running !== false : false;
339
+ }
340
+
341
+ /** Closes the stream; nothing is de-registered, because nothing was registered. */
342
+ close() {
343
+ this.closed = true;
344
+ this.ready = false;
345
+ if (this.box) {
346
+ this.box.close();
347
+ this.box = null;
348
+ }
349
+ }
350
+ }
351
+
352
+ module.exports = {
353
+ LiteConnection,
354
+ LITE_INTERFACES,
355
+ ADDON_TOKEN_FILE,
356
+ CALLBACK_METHODS,
357
+ readAddonToken,
358
+ liteValue,
359
+ tierAwareTransport,
360
+ translateError,
361
+ isFault,
362
+ load,
363
+ };
@@ -33,6 +33,7 @@
33
33
  <li>VirtualDevices: Heizungsgruppen, in CCU eingebundene Systeme wie Osram Lightify</li>
34
34
  <li>CUxD</li>
35
35
  <li>CCU-Jack: XMLRPC-Schnittstelle des <a href="https://github.com/mdzio/ccu-jack" target="_blank">CCU-Jack</a> Addons (Port konfigurierbar, Standard 2121 bzw. 2122 mit TLS)</li>
36
+ <li>Das Portfeld neben jeder Schnittstelle: leer ist der Port der CCU für den Transport (2001/42001 usw., die 3xxxx-Ports auf der CCU selbst); eine Zahl ersetzt ihn, etwa für eine CCU hinter einer Portweiterleitung oder einen Simulator auf einem anderen Port.</li>
36
37
  </ul>
37
38
 
38
39
  </li>
@@ -65,6 +66,7 @@ Weiteres unter <a target="_blank" href="https://nodered.org/docs/user-guide/cont
65
66
  </ul>
66
67
  <ul>
67
68
  <li><b>openccu-lite Port</b>: Nur nötig, wenn der Webserver des Systems nicht auf Port 80 bzw. 443 (TLS) läuft.</li>
69
+ <li><b>openccu-lite Schnittstellen</b>: Wie die Schnittstellenprozesse auf einem openccu-lite-System erreicht werden. <i>auto</i> (Voreinstellung) geht dort über das <b>lite-rpc</b> des Systems - die Aufrufe über seinen Webport, die Ereignisse aus einem Stream, den dieser Node öffnet; ein Abonnent an den Prozessen für alle Programme auf dem System, die Ereignisse werden nach einer Unterbrechung nachgeliefert, kein eigener Callback-Server und kein eigenes <code>init</code> - und auf einer CCU, RaspberryMatic oder OpenCCU den CCU-Weg. Das Zugangsmerkmal ist das openccu-lite-Token oben, auf dem System selbst das eigene Token von RedMatic, das das System dafür erzeugt; ohne eines wird die direkte Verbindung benutzt, das Log sagt es. <i>CCU</i> behält auch auf openccu-lite die direkte Verbindung mit Rückrufen (CUxD geht immer so; lite-rpc trägt es nicht). Ein Flow aus einer Version vor 4.7.0 wird beim ersten Start auf openccu-lite einmal auf lite-rpc umgestellt - das Log sagt es - und eine hier getroffene Wahl ändert keine spätere Version mehr. Die Nodes sysvar, program, script und poll bleiben auf openccu-lite so oder so ohne Funktion.</li>
68
70
  </ul>
69
71
  <ul>
70
72
  <li><b>Device data / Re-read device data</b>: Liest die Geräteliste jeder verbundenen Schnittstelle neu ein.
@@ -91,7 +93,7 @@ Weiteres unter <a target="_blank" href="https://nodered.org/docs/user-guide/cont
91
93
  solange die CCU ihr selbst ausgestelltes Zertifikat verwendet.</li>
92
94
  <li>Username: Ein Benutzer der sich an der CCU anmelden darf.</li>
93
95
  <li>Password: Das Passwort des Benutzers. Es wird in den Node-RED-Credentials gespeichert
94
- (verschlüsselt in <code>flows_cred.json</code>), nicht im Flow. Ein Flow aus einer Version vor 4.4.6,
96
+ (verschlüsselt in <code>flows_cred.json</code>), nicht im Flow. Ein Flow aus einer Version vor 4.5.0,
95
97
  der es noch im Klartext enthält, wird beim ersten Start übernommen; der nächste Deploy entfernt es aus
96
98
  <code>flows.json</code>.</li>
97
99
  </ul>
@@ -18,6 +18,7 @@
18
18
  <li>VirtualDevices: heating groups and systems the CCU integrates, such as Osram Lightify</li>
19
19
  <li>CUxD</li>
20
20
  <li>CCU-Jack: the XML-RPC interface of the <a href="https://github.com/mdzio/ccu-jack" target="_blank">CCU-Jack</a> addon (port configurable, 2121 by default, 2122 with TLS)</li>
21
+ <li>The port field beside each interface: blank is the CCU's port for the transport (2001/42001 and so on, the 3xxxx ports on the CCU itself); a number replaces it, for a CCU behind port forwarding or a simulator on another port.</li>
21
22
  </ul>
22
23
  </li>
23
24
  <li><b>RPC ping</b> and <b>RPC ping timeout (s)</b>: How often the connection to the interface processes is checked with a ping, and after how many seconds without an answer it subscribes again. Off means no pings and no timeout-triggered re-init for this connection.</li>
@@ -27,6 +28,7 @@
27
28
  <li><b>Context Store</b>: The Node-RED context store the states of all devices and variables are kept in. See <a target="_blank" href="https://nodered.org/docs/user-guide/context">Working with context</a> in the Node-RED documentation.</li>
28
29
  <li><b>openccu-lite token</b>: Only for <a href="https://github.com/hobbyquaker/openccu-lite" target="_blank">openccu-lite</a>, a CCU firmware without ReGaHSS. Names, rooms and functions come from the system's metadata API there, which needs a token made under <i>Users</i> on the system. When Node-RED runs on the system itself (RedMatic) the field stays empty: the local token is read automatically. The token is kept, like the password, in Node-RED's credentials (encrypted in <code>flows_cred.json</code>, never in the flow, never in an export). A CCU, RaspberryMatic or OpenCCU ignores the field. openccu-lite has no system variables, programs or HM-Script; those nodes stay in the flow and report an error per message.</li>
29
30
  <li><b>openccu-lite port</b>: Only needed when the system's web server is not on port 80 (443 with TLS).</li>
31
+ <li><b>openccu-lite interfaces</b>: How the interface processes are reached on an openccu-lite system. <i>auto</i> (the default) goes through the system's <b>lite-rpc</b> there - the requests over its web port, the events from a stream this node opens, one subscriber at the processes for every program on the system, the events replayed after a break, no callback server and no <code>init</code> of its own - and the CCU way on a CCU, RaspberryMatic or OpenCCU. The credential is the openccu-lite token above, or on the system itself RedMatic's own token, which the system mints for it; without one the direct connection is used and the log says so. <i>CCU</i> keeps the direct connection with callbacks on openccu-lite as well (CUxD always goes that way; lite-rpc does not carry it). A flow from before 4.7.0 is moved to lite-rpc once at its first start on openccu-lite - the log says so - and a choice made here is never changed by a later version. The sysvar, program, script and poll nodes stay unavailable on openccu-lite either way.</li>
30
32
  <li><b>Device data / Re-read device data</b>: Reads the device list of every connected interface again. New and changed devices (a channel whose type changed, say) get their datapoints fetched anew, removed ones leave the table. A device the CCU reports with a changed type or version is re-read without the button as well. The button needs a deployed connection.</li>
31
33
  <li><b>Authentication</b>: Encryption and login for a CCU on the network (Node-RED not on the CCU). Both belong to the CCU's web server, which fronts the interface processes: on a CCU3 TLS and authentication are switched on in the firewall settings, on openccu-lite under <i>Remote access</i> (classic RPC). When Node-RED runs on the system itself (RedMatic; address <code>localhost</code> / <code>127.0.0.1</code>) the processes are reached directly on the loopback, BidCos over BIN-RPC, without TLS and without a login; these settings are then not used.
32
34
  <ul>
@@ -34,7 +36,7 @@
34
36
  <li>TLS/SSL: Transport encryption. The interfaces are reached over the CCU's TLS ports (42000, 42001, 42010, 49292, ReGaHSS 48181, CCU-Jack 2122), openccu-lite's metadata API over 443. BIN-RPC has no TLS: with TLS on, BidCos-RF always goes over XML-RPC.</li>
35
37
  <li>Ignore invalid TLS Certificates: Node-RED accepts an untrusted certificate too. Needed as long as the CCU uses its self-signed certificate.</li>
36
38
  <li>Username: A user allowed to log in to the CCU.</li>
37
- <li>Password: The user's password. It is kept in Node-RED's credentials (encrypted in <code>flows_cred.json</code>), not in the flow. A flow from a version before 4.4.6 that still carries it in plain text is taken over at the first start; the next deploy removes it from <code>flows.json</code>.</li>
39
+ <li>Password: The user's password. It is kept in Node-RED's credentials (encrypted in <code>flows_cred.json</code>), not in the flow. A flow from a version before 4.5.0 that still carries it in plain text is taken over at the first start; the next deploy removes it from <code>flows.json</code>.</li>
38
40
  </ul>
39
41
  The other direction stays unencrypted: the CCU sends its events to the XML-RPC or BIN-RPC server Node-RED opens for it (<i>Listen Address</i>, <i>BINRPC</i> and <i>XMLRPC listening port</i>) without TLS and without a login - it cannot log in there or verify our certificate. Those ports should be open in the Node-RED host's firewall for the CCU only.</li>
40
42
  </ul>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-ccu",
3
- "version": "4.6.2",
3
+ "version": "4.7.1",
4
4
  "license": "MIT",
5
5
  "main": "none",
6
6
  "keywords": [
@@ -53,7 +53,8 @@
53
53
  "binrpc": "^4.3.0",
54
54
  "homematic-rega": "^2.0.1",
55
55
  "homematic-xmlrpc": "^2.0.0",
56
- "mqtt-wildcard": "^3.0.9"
56
+ "mqtt-wildcard": "^3.0.9",
57
+ "occulite-client": "^0.3.0"
57
58
  },
58
59
  "deprecated": false,
59
60
  "description": "Node-RED Nodes for the Homematic CCU",
@@ -64,7 +65,7 @@
64
65
  "eslint-config-prettier": "^10.1.8",
65
66
  "eslint-plugin-html": "^8.2.0",
66
67
  "globals": "^16.5.0",
67
- "hm-simulator": "^1.3.1",
68
+ "hm-simulator": "^1.4.0",
68
69
  "mocha": "^11.7.1",
69
70
  "node-red": "^4.1.0",
70
71
  "node-red-node-test-helper": "^0.3.6",