node-red-contrib-ccu 4.6.1 → 4.7.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/CHANGELOG.md +88 -31
- package/README.md +16 -2
- package/nodes/ccu-connection.html +28 -7
- package/nodes/ccu-connection.js +412 -23
- package/nodes/lib/initretry.js +72 -17
- package/nodes/lib/lite.js +309 -0
- package/nodes/locales/de/ccu-connection.html +3 -1
- package/nodes/locales/en-US/ccu-connection.html +3 -1
- package/package.json +4 -3
package/nodes/lib/initretry.js
CHANGED
|
@@ -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
|
|
9
|
-
*
|
|
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
|
|
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}
|
|
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(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
34
|
+
function retryDelay(elapsed) {
|
|
35
|
+
return elapsed < RETRY_WINDOW ? RETRY_INTERVAL : RETRY_MAX_DELAY;
|
|
36
|
+
}
|
|
32
37
|
|
|
33
|
-
|
|
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.
|
|
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
|
-
() =>
|
|
184
|
-
|
|
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
|
-
|
|
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 = {
|
|
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,309 @@
|
|
|
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 Boolean(error) && error.kind === 'rpc-fault' && error.faultCode !== undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The client's error as the connection reports it: a fault keeps its code and string, a refused
|
|
88
|
+
* scope says which, everything else its kind.
|
|
89
|
+
* @param {*} error
|
|
90
|
+
* @returns {Error}
|
|
91
|
+
*/
|
|
92
|
+
function translateError(error) {
|
|
93
|
+
if (isFault(error)) {
|
|
94
|
+
const fault = new Error(error.faultString || error.message);
|
|
95
|
+
fault.faultCode = error.faultCode;
|
|
96
|
+
fault.faultString = error.faultString || error.message;
|
|
97
|
+
return fault;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (error && error.kind === 'forbidden') {
|
|
101
|
+
const refused = new Error(
|
|
102
|
+
'the openccu-lite token may not call this method' + (error.scope ? ' (needs ' + error.scope + ')' : ''),
|
|
103
|
+
);
|
|
104
|
+
refused.kind = 'forbidden';
|
|
105
|
+
refused.scope = error.scope;
|
|
106
|
+
return refused;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (error && error.kind) {
|
|
110
|
+
const translated = new Error(error.message || error.kind);
|
|
111
|
+
translated.kind = error.kind;
|
|
112
|
+
return translated;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const RPC_TIERS = ['rpc:operate', 'rpc:configure', 'rpc:admin'];
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* occulite-client B-3: the client decides "names only" from the literal `rpc:read` in
|
|
122
|
+
* GET /api/auth/v1/state, but a token made with a higher tier (rpc:admin, say) carries that tier
|
|
123
|
+
* alone - the system includes the lower ones. The transport spells `rpc:read` out for the client
|
|
124
|
+
* where a tier implies it. Goes once the package fixes it.
|
|
125
|
+
* @param {object} transport occulite-client's Transport
|
|
126
|
+
* @returns {object} the same transport
|
|
127
|
+
*/
|
|
128
|
+
function tierAwareTransport(transport) {
|
|
129
|
+
return {
|
|
130
|
+
request: (method, url, headers, body, signal) =>
|
|
131
|
+
transport.request(method, url, headers, body, signal).then((response) => {
|
|
132
|
+
if (response.status !== 200 || !/\/api\/auth\/v1\/state(\?|$)/.test(String(url))) {
|
|
133
|
+
return response;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let parsed;
|
|
137
|
+
try {
|
|
138
|
+
parsed = JSON.parse(response.body);
|
|
139
|
+
} catch {
|
|
140
|
+
return response;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const scopes = Array.isArray(parsed && parsed.scopes) ? parsed.scopes : [];
|
|
144
|
+
if (scopes.includes('rpc:read') || !scopes.some((scope) => RPC_TIERS.includes(scope))) {
|
|
145
|
+
return response;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {...response, body: JSON.stringify({...parsed, scopes: [...scopes, 'rpc:read']})};
|
|
149
|
+
}),
|
|
150
|
+
stream: (url, headers, signal) => transport.stream(url, headers, signal),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
class LiteConnection {
|
|
155
|
+
/**
|
|
156
|
+
* @param {object} options
|
|
157
|
+
* @param {string} options.url the system, scheme and authority
|
|
158
|
+
* @param {string} options.token the credential
|
|
159
|
+
* @param {boolean} [options.insecure] accept any certificate
|
|
160
|
+
* @param {string[]} options.interfaces the interfaces to follow (the enabled ones lite-rpc carries)
|
|
161
|
+
* @param {function(string): string} options.initId the init id the connection's callback table expects per interface
|
|
162
|
+
* @param {function(string, *, Array, Function)} options.onCallback (method, err, params, answer): a callback server's call
|
|
163
|
+
* @param {function(string, string)} options.onInterface (iface, state): up | down | restarted | added | removed
|
|
164
|
+
* @param {function(string)} options.onStatus the client's state: connecting | ready | degraded | offline | closed
|
|
165
|
+
* @param {function(Map)} [options.onValues] box.values once ready (the state store's picture)
|
|
166
|
+
* @param {object} options.logger {debug, info, warn, error}
|
|
167
|
+
* @param {number} [options.timeout] per request, ms
|
|
168
|
+
* @param {Function} [options.createClient] for tests, in place of new OccuLite()
|
|
169
|
+
*/
|
|
170
|
+
constructor(options) {
|
|
171
|
+
this.options = options;
|
|
172
|
+
this.interfaces = [...options.interfaces];
|
|
173
|
+
this.logger = options.logger;
|
|
174
|
+
this.box = null;
|
|
175
|
+
this.server = null;
|
|
176
|
+
this.closed = false;
|
|
177
|
+
this.ready = false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {string} iface
|
|
182
|
+
* @returns {boolean} whether this interface goes through lite-rpc
|
|
183
|
+
*/
|
|
184
|
+
has(iface) {
|
|
185
|
+
return this.interfaces.includes(iface);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Opens the client. Resolves once the first picture is loaded, rejects with the client's
|
|
190
|
+
* reason (unauthenticated, forbidden, unreachable, timeout ...); the stream keeps trying by
|
|
191
|
+
* itself afterwards.
|
|
192
|
+
* @returns {Promise<void>}
|
|
193
|
+
*/
|
|
194
|
+
start() {
|
|
195
|
+
const {OccuLite, callbackEvents, nodeTransport} = load();
|
|
196
|
+
const debug = (message) => this.logger.debug('occulite-client: ' + message);
|
|
197
|
+
const clientOptions = {
|
|
198
|
+
url: this.options.url,
|
|
199
|
+
auth: {token: this.options.token},
|
|
200
|
+
transport: tierAwareTransport(nodeTransport(this.options.insecure ? {insecure: true} : {})),
|
|
201
|
+
interfaces: this.interfaces,
|
|
202
|
+
values: true,
|
|
203
|
+
meta: false,
|
|
204
|
+
devices: true,
|
|
205
|
+
timeout: this.options.timeout || 30000,
|
|
206
|
+
log: {debug, info: debug, warn: debug, error: debug},
|
|
207
|
+
};
|
|
208
|
+
const box = this.options.createClient ? this.options.createClient(clientOptions) : new OccuLite(clientOptions);
|
|
209
|
+
this.box = box;
|
|
210
|
+
box.on('status', (event) => {
|
|
211
|
+
if (this.closed || this.box !== box) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const state = event && event.state ? event.state : event;
|
|
216
|
+
this.ready = state === 'ready';
|
|
217
|
+
this.options.onStatus(state);
|
|
218
|
+
});
|
|
219
|
+
box.on('interface', (event) => {
|
|
220
|
+
if (!this.closed && this.box === box) {
|
|
221
|
+
this.options.onInterface(event.interface, event.state);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
box.on('resync', () => {
|
|
225
|
+
if (!this.closed && this.box === box && this.options.onValues) {
|
|
226
|
+
this.options.onValues(box.values);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
// the stream's events as a callback server's calls, into the connection's own table
|
|
230
|
+
this.server = callbackEvents(box, {initId: this.options.initId});
|
|
231
|
+
CALLBACK_METHODS.forEach((method) => {
|
|
232
|
+
this.server.on(method, (err, parameters, callback) => {
|
|
233
|
+
if (this.closed || this.box !== box) {
|
|
234
|
+
callback(null, '');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this.options.onCallback(method, err, parameters, callback);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return box.ready().then(() => {
|
|
243
|
+
if (this.closed || this.box !== box) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
this.ready = true;
|
|
248
|
+
if (this.options.onValues) {
|
|
249
|
+
this.options.onValues(box.values);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Any method of an interface process through lite-rpc; the credential's tier decides.
|
|
256
|
+
* @param {string} iface
|
|
257
|
+
* @param {string} method
|
|
258
|
+
* @param {Array} parameters
|
|
259
|
+
* @returns {Promise<*>}
|
|
260
|
+
*/
|
|
261
|
+
call(iface, method, parameters) {
|
|
262
|
+
if (!this.box) {
|
|
263
|
+
return Promise.reject(new Error('lite-rpc client not started'));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return this.box.call(iface, method, parameters || []).then(
|
|
267
|
+
(result) => (result === null || result === undefined ? '' : result),
|
|
268
|
+
(error) => {
|
|
269
|
+
throw translateError(error);
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Whether the system says this interface runs (the client's live list).
|
|
276
|
+
* @param {string} iface
|
|
277
|
+
* @returns {boolean|undefined} undefined while the list is not known
|
|
278
|
+
*/
|
|
279
|
+
running(iface) {
|
|
280
|
+
if (!this.box || !Array.isArray(this.box.interfaces)) {
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const entry = this.box.interfaces.find((info) => info.name === iface);
|
|
285
|
+
return entry ? entry.running !== false : false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Closes the stream; nothing is de-registered, because nothing was registered. */
|
|
289
|
+
close() {
|
|
290
|
+
this.closed = true;
|
|
291
|
+
this.ready = false;
|
|
292
|
+
if (this.box) {
|
|
293
|
+
this.box.close();
|
|
294
|
+
this.box = null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = {
|
|
300
|
+
LiteConnection,
|
|
301
|
+
LITE_INTERFACES,
|
|
302
|
+
ADDON_TOKEN_FILE,
|
|
303
|
+
CALLBACK_METHODS,
|
|
304
|
+
readAddonToken,
|
|
305
|
+
tierAwareTransport,
|
|
306
|
+
translateError,
|
|
307
|
+
isFault,
|
|
308
|
+
load,
|
|
309
|
+
};
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
3
|
+
"version": "4.7.0",
|
|
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.
|
|
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",
|