hm2mqtt 3.5.1 → 3.6.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/README.md +45 -1
- package/config.js +10 -0
- package/index.js +168 -35
- package/lib/hadiscovery.js +15 -3
- package/lib/interfaces.js +4 -6
- package/lib/meta.js +724 -0
- package/lib/names.js +49 -0
- package/lib/rega.js +4 -24
- package/lib/topics.js +34 -12
- package/package.json +3 -2
- package/scripts/addon-api.js +2 -13
package/README.md
CHANGED
|
@@ -259,7 +259,51 @@ the plain format (the second `ccu-mqtt` node of a flow).
|
|
|
259
259
|
Names come from the ReGa (devices, channels, rooms, functions), are cached in the state
|
|
260
260
|
directory and re-read with `<name>/set/rega/sync`. A `--name-file` overrides single addresses.
|
|
261
261
|
Without the ReGa (`--no-rega`) topics use addresses. Events of channels without a name use the
|
|
262
|
-
address as well.
|
|
262
|
+
address as well. On a box without ReGaHSS they come from the metadata API instead — see
|
|
263
|
+
[openccu-lite](#openccu-lite).
|
|
264
|
+
|
|
265
|
+
## openccu-lite
|
|
266
|
+
|
|
267
|
+
[openccu-lite](https://github.com/hobbyquaker/openccu-lite) is a CCU firmware without ReGaHSS.
|
|
268
|
+
hm2mqtt works there without a changed configuration: at start (and again on reconnect) it asks
|
|
269
|
+
`GET http://<ccu-address>/api/meta/v1/version`, and when that answers it takes device and channel
|
|
270
|
+
names, rooms and functions from the box's **metadata API** instead of from the ReGa. A CCU3,
|
|
271
|
+
RaspberryMatic or OpenCCU answers 404 there and everything stays exactly as before — the same
|
|
272
|
+
configuration works on both, which is what a backup restored on the other kind of box needs.
|
|
273
|
+
|
|
274
|
+
Names arrive as a snapshot at start and then over the box's event stream (`/api/meta/v1/events/sse`):
|
|
275
|
+
a rename in the box's UI is in the topics about a second later, without a poll and without a
|
|
276
|
+
restart. The store is cached in `meta.json` in the state directory, so a start without the box
|
|
277
|
+
still has the names. `<name>/set/rega/sync` re-reads the snapshot here too.
|
|
278
|
+
|
|
279
|
+
**The credential.** Everything but the version call needs one:
|
|
280
|
+
|
|
281
|
+
| where hm2mqtt runs | credential |
|
|
282
|
+
| ------------------ | ----------------------------------------------------------------------------------------------------- |
|
|
283
|
+
| on the box (addon) | the box's own read-only token, read from `/usr/local/etc/occulite/local-token` — nothing to configure |
|
|
284
|
+
| anywhere else | `--meta-token` (`HM2MQTT_META_TOKEN`): an API token (`olt_…`) created on the box under _Benutzer_ |
|
|
285
|
+
|
|
286
|
+
Without a valid credential hm2mqtt logs one line and runs on addresses; it picks the names up as
|
|
287
|
+
soon as a token works, without a restart. `--meta-url` overrides the base url when the box is
|
|
288
|
+
behind a proxy or on another port. Note that openccu-lite's interface processes listen on loopback
|
|
289
|
+
only, so hm2mqtt normally runs **on** the box (addon package) or goes through its XML-RPC proxy.
|
|
290
|
+
|
|
291
|
+
**What has no replacement** on such a box (from openccu-lite's `docs/porting-from-rega.md`):
|
|
292
|
+
|
|
293
|
+
- **System variables** and **programs**: there is no ReGa DOM. Users who need them run automation
|
|
294
|
+
in Node-RED (RedMatic), Home Assistant, or whatever the addon is bridging to.
|
|
295
|
+
- **`exec()`** of HM-Script, `dom.GetObject`, `system.GetSessionVarStr` from your own code: gone.
|
|
296
|
+
- **ReGa ids** (`dom.GetObject(1234)`): there are none. Refs (`<interface>.<address>`) are the identity.
|
|
297
|
+
- **Service messages / alarms** (variables 40 and 41): interface-level state only.
|
|
298
|
+
- **The CCU WebUI's JSON-RPC API** (`/api/homematic.cgi`, `Session.login`, `Device.listAll`,
|
|
299
|
+
`Interface.*`): not present.
|
|
300
|
+
|
|
301
|
+
In hm2mqtt that means: no `status/<variable>` and `status/<program>` topics and no writes to them,
|
|
302
|
+
and no `--publish-cache` (the value cache was ReGa's `getValues`). `--rega-poll-interval`,
|
|
303
|
+
`--rega-poll-trigger` and the `--topic-sysvar-*`/`--topic-program-*` templates stay accepted so one
|
|
304
|
+
configuration serves both kinds of box; on openccu-lite they do nothing and one log line at start
|
|
305
|
+
says so. Rooms and functions are trees there — a channel in `room/eg/wohnzimmer` is reported as
|
|
306
|
+
`Wohnzimmer`, the parent nodes are not added, so `hm.rooms` looks exactly as it did on a CCU.
|
|
263
307
|
|
|
264
308
|
## Home Assistant
|
|
265
309
|
|
package/config.js
CHANGED
|
@@ -72,6 +72,16 @@ export const OPTIONS = {
|
|
|
72
72
|
type: 'string',
|
|
73
73
|
describe: 'channel.datapoint whose event triggers a variable/program poll, e.g. BidCoS-RF:50.PRESS_SHORT',
|
|
74
74
|
},
|
|
75
|
+
'meta-token': {
|
|
76
|
+
type: 'string',
|
|
77
|
+
describe:
|
|
78
|
+
'openccu-lite API token (olt_...) for names, rooms and functions - not needed on the box itself, where the local token is read',
|
|
79
|
+
secret: true,
|
|
80
|
+
},
|
|
81
|
+
'meta-url': {
|
|
82
|
+
type: 'string',
|
|
83
|
+
describe: "base url of openccu-lite's metadata api (default: http[s]://<ccu-address>)",
|
|
84
|
+
},
|
|
75
85
|
'ccu-timezone': {type: 'string', describe: "IANA time zone of the CCU (default: this host's time zone)"},
|
|
76
86
|
'name-file': {
|
|
77
87
|
alias: 'm',
|
package/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import {RpcServers, RpcConnection} from './lib/rpc.js';
|
|
|
20
20
|
import {Metadata} from './lib/metadata.js';
|
|
21
21
|
import {ValueStore, hmBlock, isEvent} from './lib/values.js';
|
|
22
22
|
import {RegaSync} from './lib/rega.js';
|
|
23
|
+
import {MetaSync, detectMeta} from './lib/meta.js';
|
|
23
24
|
import {castValue, isWriteable} from './lib/cast.js';
|
|
24
25
|
import {
|
|
25
26
|
sanitizeName,
|
|
@@ -66,6 +67,8 @@ const SET_THROTTLE_MS = 500;
|
|
|
66
67
|
const DEVICES_WAIT_MS = 10000;
|
|
67
68
|
const STOP_TIMEOUT_MS = 1500;
|
|
68
69
|
const RESOLVE_RETRY_MS = 10000;
|
|
70
|
+
/** how often an unreachable box is probed for the metadata api again (H-47) */
|
|
71
|
+
const REDETECT_MS = 60000;
|
|
69
72
|
|
|
70
73
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
71
74
|
/** the configured CCU address (the `ccu` field of every payload); connections use the resolved ip */
|
|
@@ -149,11 +152,17 @@ const renderSysvarStatus = compileTemplate(config.topicSysvarStatus);
|
|
|
149
152
|
const renderSysvarSet = compileTemplate(config.topicSysvarSet);
|
|
150
153
|
const renderProgramStatus = compileTemplate(config.topicProgramStatus);
|
|
151
154
|
const renderProgramSet = compileTemplate(config.topicProgramSet);
|
|
152
|
-
/**
|
|
155
|
+
/** the literal part of the status template - what every rendered status topic starts with */
|
|
156
|
+
const statusLiteral = subscribePattern(config.topicStatus, topicFields).replace(/#$/, '');
|
|
157
|
+
/** what the plain mirror tree calls an item: the status topic without the template's literal head */
|
|
153
158
|
const plainItem = (topic) => {
|
|
154
|
-
const
|
|
155
|
-
return
|
|
159
|
+
const remainder = templateRemainder(config.topicStatus, topic, topicFields);
|
|
160
|
+
return remainder === null || remainder === '' ? topic : remainder;
|
|
156
161
|
};
|
|
162
|
+
/** the set subscriptions that leave `<name>/set/` and therefore need a listen pattern */
|
|
163
|
+
const listenPatterns = [...new Set([config.topicSet, config.topicSysvarSet, config.topicProgramSet])]
|
|
164
|
+
.map((template) => subscribePattern(template, topicFields))
|
|
165
|
+
.filter((pattern) => !pattern.startsWith(`${config.name}/set/`));
|
|
157
166
|
const adapter = createAdapter({
|
|
158
167
|
pkg,
|
|
159
168
|
config,
|
|
@@ -180,6 +189,7 @@ const adapter = createAdapter({
|
|
|
180
189
|
interfaces: enabled,
|
|
181
190
|
devices: metadata.count(),
|
|
182
191
|
rega: Boolean(regaSync),
|
|
192
|
+
names: regaSync ? regaSync.label : 'addresses',
|
|
183
193
|
payload: payloadFormat,
|
|
184
194
|
}),
|
|
185
195
|
/*
|
|
@@ -190,12 +200,7 @@ const adapter = createAdapter({
|
|
|
190
200
|
* the index is keyed by.
|
|
191
201
|
*/
|
|
192
202
|
onSet: (parts, value, topic) => handleSetTopic(topic, value),
|
|
193
|
-
listen: Object.fromEntries(
|
|
194
|
-
[...new Set([config.topicSet, config.topicSysvarSet, config.topicProgramSet])]
|
|
195
|
-
.map((template) => subscribePattern(template, topicFields))
|
|
196
|
-
.filter((pattern) => !pattern.startsWith(`${config.name}/set/`))
|
|
197
|
-
.map((pattern) => [pattern, handleSetTopic]),
|
|
198
|
-
),
|
|
203
|
+
listen: Object.fromEntries(listenPatterns.map((pattern) => [pattern, handleSetTopic])),
|
|
199
204
|
subscriptions: {
|
|
200
205
|
'paramset/#': handleParamset,
|
|
201
206
|
...(config.rpcTopics ? {'rpc/+/+/+': handleRpc} : {}),
|
|
@@ -205,6 +210,21 @@ const adapter = createAdapter({
|
|
|
205
210
|
});
|
|
206
211
|
const {log, pubStatus} = adapter;
|
|
207
212
|
|
|
213
|
+
for (const warning of config.$warnings || []) {
|
|
214
|
+
log.warn(warning);
|
|
215
|
+
}
|
|
216
|
+
if (config.topicStatus === config.topicSet) {
|
|
217
|
+
log.warn('--topic-status and --topic-set are identical: every own publication would come back as a set');
|
|
218
|
+
}
|
|
219
|
+
for (const pattern of listenPatterns) {
|
|
220
|
+
const literal = pattern.replace(/#$/, '');
|
|
221
|
+
if (statusLiteral.startsWith(literal)) {
|
|
222
|
+
log.warn(
|
|
223
|
+
`the set subscription ${pattern} also matches the status topics under ${statusLiteral} - own publications there are ignored`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
208
228
|
/*
|
|
209
229
|
* Running on the CCU itself, the interface processes are on loopback and the familiar
|
|
210
230
|
* 2000/2001/2010/9292/8181 are only lighttpd proxies in front of them: an extra hop, XML over HTTP
|
|
@@ -212,7 +232,9 @@ const {log, pubStatus} = adapter;
|
|
|
212
232
|
* default we probe, because node-red-contrib-ccu's config-file check stopped working on current
|
|
213
233
|
* firmware (see lib/interfaces.js).
|
|
214
234
|
*/
|
|
215
|
-
|
|
235
|
+
// an explicit --ccu-tls is a tunnel or a proxy by definition - local mode would silently drop the
|
|
236
|
+
// TLS and the ports the user asked for, so it is never probed into, only chosen with --local
|
|
237
|
+
const localMode = config.local === undefined ? !config.ccuTls && (await detectLocal(host)) : Boolean(config.local);
|
|
216
238
|
if (localMode) {
|
|
217
239
|
log.info('local mode: BidCos over binrpc (32001/32000), hmipserver on 32010, ReGa on 8183');
|
|
218
240
|
}
|
|
@@ -220,8 +242,20 @@ if (localMode) {
|
|
|
220
242
|
const metadata = new Metadata({stateDir: config.stateDir, seedFile: path.join(here, 'paramsets.json'), log});
|
|
221
243
|
metadata.load();
|
|
222
244
|
|
|
245
|
+
/*
|
|
246
|
+
* Where names, rooms and functions come from (H-46): openccu-lite has no ReGaHSS, but a metadata
|
|
247
|
+
* api that answers `GET /api/meta/v1/version` and nothing else does. That one call is the whole
|
|
248
|
+
* detection - no new option, no hostname or port heuristics, so a configuration moved between a
|
|
249
|
+
* CCU and an openccu-lite box keeps working untouched. Explicit --no-rega still means "addresses
|
|
250
|
+
* only" on both.
|
|
251
|
+
*/
|
|
252
|
+
const metaUrl = config.metaUrl || `${config.ccuTls ? 'https' : 'http'}://${host}`;
|
|
253
|
+
/** true while the box has not answered the probe at all: something may still come up there */
|
|
254
|
+
let redetect = false;
|
|
255
|
+
let redetectTimer = null;
|
|
223
256
|
let regaSync = null;
|
|
224
|
-
|
|
257
|
+
|
|
258
|
+
function createRega() {
|
|
225
259
|
const rega = new Rega({
|
|
226
260
|
host: ccuIp,
|
|
227
261
|
port: regaPort({tls: config.ccuTls, local: localMode}),
|
|
@@ -231,7 +265,37 @@ if (config.rega) {
|
|
|
231
265
|
password: config.ccuPassword,
|
|
232
266
|
timeZone: config.ccuTimezone,
|
|
233
267
|
});
|
|
234
|
-
|
|
268
|
+
return new RegaSync({rega, host, metadata, stateDir: config.stateDir, nameFile, log});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function createMeta() {
|
|
272
|
+
return new MetaSync({
|
|
273
|
+
url: metaUrl,
|
|
274
|
+
token: config.metaToken,
|
|
275
|
+
insecure: config.ccuInsecure,
|
|
276
|
+
fixedUrl: Boolean(config.metaUrl),
|
|
277
|
+
metadata,
|
|
278
|
+
stateDir: config.stateDir,
|
|
279
|
+
nameFile,
|
|
280
|
+
log,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (config.rega) {
|
|
285
|
+
const found = await detectMeta(metaUrl, {insecure: config.ccuInsecure});
|
|
286
|
+
if (found.ok) {
|
|
287
|
+
log.info(
|
|
288
|
+
'openccu-lite on',
|
|
289
|
+
host + ':',
|
|
290
|
+
found.version.implementation || 'metadata api v' + found.version.version,
|
|
291
|
+
'- names, rooms and functions from the metadata api',
|
|
292
|
+
);
|
|
293
|
+
regaSync = createMeta();
|
|
294
|
+
} else {
|
|
295
|
+
log.debug('no metadata api on', host, '(' + found.reason + ') - using ReGa');
|
|
296
|
+
redetect = !found.reachable;
|
|
297
|
+
regaSync = createRega();
|
|
298
|
+
}
|
|
235
299
|
regaSync.load();
|
|
236
300
|
}
|
|
237
301
|
|
|
@@ -560,11 +624,21 @@ async function handleSetTopic(topic, value) {
|
|
|
560
624
|
}
|
|
561
625
|
let target = topicIndex.get(topic);
|
|
562
626
|
if (!target) {
|
|
563
|
-
|
|
627
|
+
// the reserved namespace stays positional whatever the template says: the address form,
|
|
628
|
+
// the rega commands and the sysvar/program names below <name>/set/ keep working
|
|
629
|
+
const head = `${config.name}/set/`;
|
|
630
|
+
const remainder = topic.startsWith(head)
|
|
631
|
+
? topic.slice(head.length)
|
|
632
|
+
: templateRemainder(config.topicSet, topic, topicFields);
|
|
564
633
|
const positional = remainder === null ? null : remainder.split('/').filter(Boolean);
|
|
565
634
|
target = positional && positional.length > 0 ? resolveSet(positional, lookup) : null;
|
|
566
635
|
}
|
|
567
636
|
if (!target) {
|
|
637
|
+
// a set subscription that overlaps the status tree (a template whose literal part ends
|
|
638
|
+
// above it) delivers our own publications here - those are not errors
|
|
639
|
+
if (statusLiteral && topic.startsWith(statusLiteral)) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
568
642
|
throw new Error('unknown channel, variable or program');
|
|
569
643
|
}
|
|
570
644
|
switch (target.kind) {
|
|
@@ -778,56 +852,113 @@ function createConnection(iface) {
|
|
|
778
852
|
*/
|
|
779
853
|
|
|
780
854
|
async function startRega() {
|
|
855
|
+
if (regaSync instanceof MetaSync) {
|
|
856
|
+
// one line, once: everything a ReGa box could do and this one cannot (porting invariant 4).
|
|
857
|
+
// The options stay accepted so a configuration works on both kinds of box.
|
|
858
|
+
log.info(
|
|
859
|
+
'occulite: no ReGaHSS on this box - system variables, programs and the value cache are',
|
|
860
|
+
'not available; --rega-poll-interval, --rega-poll-trigger, --publish-cache and the',
|
|
861
|
+
'sysvar/program topics do nothing here',
|
|
862
|
+
);
|
|
863
|
+
}
|
|
781
864
|
try {
|
|
782
865
|
await regaSync.syncNames();
|
|
783
866
|
} catch (err) {
|
|
784
867
|
log.warn(
|
|
785
|
-
'
|
|
868
|
+
regaSync.label + ': names not available (' + err.message + '), using',
|
|
786
869
|
Object.keys(regaSync.channelNames).length,
|
|
787
870
|
'cached names',
|
|
788
871
|
);
|
|
789
872
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
)
|
|
793
|
-
|
|
794
|
-
.
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
873
|
+
warnDuplicateNames();
|
|
874
|
+
attachProvider();
|
|
875
|
+
regaSync.startPolling(config.regaPollInterval);
|
|
876
|
+
if (regaSync.rega) {
|
|
877
|
+
loadCache().catch((err) => log.warn('rega getValues failed:', err.message));
|
|
878
|
+
}
|
|
879
|
+
if (config.regaNamesInterval > 0) {
|
|
880
|
+
timers.push(setInterval(() => resyncNames(), config.regaNamesInterval * 1000));
|
|
881
|
+
}
|
|
882
|
+
if (redetect) {
|
|
883
|
+
// nothing answered the probe at start (the box was down or still booting): ReGa was the
|
|
884
|
+
// fallback, and an openccu-lite that comes up later must not need a restart of this
|
|
885
|
+
// service to be recognised
|
|
886
|
+
redetectTimer = setInterval(
|
|
887
|
+
() => redetectProvider().catch((err) => log.debug('re-detection failed:', err.message)),
|
|
888
|
+
REDETECT_MS,
|
|
800
889
|
);
|
|
890
|
+
timers.push(redetectTimer);
|
|
801
891
|
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/** The provider's events, wired the same way whichever provider it is. */
|
|
895
|
+
function attachProvider() {
|
|
802
896
|
regaSync.on('names', scheduleIndex);
|
|
803
897
|
regaSync.on('sysvar', publishRega);
|
|
804
898
|
regaSync.on('program', publishRega);
|
|
805
899
|
regaSync.on('polled', () => {
|
|
806
900
|
if (!regaOk) {
|
|
807
901
|
regaOk = true;
|
|
808
|
-
log.info('
|
|
902
|
+
log.info(regaSync.label + ' connected');
|
|
809
903
|
updateConnected();
|
|
810
904
|
}
|
|
811
905
|
});
|
|
812
906
|
regaSync.on('error', () => {
|
|
813
907
|
if (regaOk) {
|
|
814
908
|
regaOk = false;
|
|
815
|
-
log.warn('
|
|
909
|
+
log.warn(regaSync.label + ' disconnected');
|
|
816
910
|
updateConnected();
|
|
817
911
|
}
|
|
818
912
|
});
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function warnDuplicateNames() {
|
|
916
|
+
const duplicates = Object.entries(
|
|
917
|
+
Object.values(regaSync.channelNames).reduce((acc, name) => ((acc[name] = (acc[name] || 0) + 1), acc), {}),
|
|
918
|
+
)
|
|
919
|
+
.filter(([, n]) => n > 1)
|
|
920
|
+
.map(([name]) => name);
|
|
921
|
+
if (duplicates.length > 0) {
|
|
922
|
+
log.warn(
|
|
923
|
+
'duplicate channel names share a topic:',
|
|
924
|
+
duplicates.slice(0, 20).join(', '),
|
|
925
|
+
duplicates.length > 20 ? '…' : '',
|
|
827
926
|
);
|
|
828
927
|
}
|
|
829
928
|
}
|
|
830
929
|
|
|
930
|
+
function resyncNames() {
|
|
931
|
+
return regaSync.syncNames().catch((err) => log.warn(regaSync.label + ' names re-sync failed:', err.message));
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/** Probes a box that did not answer at start; switches to the metadata api when it now does. */
|
|
935
|
+
async function redetectProvider() {
|
|
936
|
+
const found = await detectMeta(metaUrl, {insecure: config.ccuInsecure});
|
|
937
|
+
if (!found.reachable) {
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
// something answers now - whatever it is, this question is settled
|
|
941
|
+
redetect = false;
|
|
942
|
+
if (redetectTimer) {
|
|
943
|
+
clearInterval(redetectTimer);
|
|
944
|
+
redetectTimer = null;
|
|
945
|
+
}
|
|
946
|
+
if (!found.ok) {
|
|
947
|
+
log.debug('no metadata api on', host, '(' + found.reason + ') - staying with ReGa');
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
log.info('openccu-lite answered on', host + ': switching from ReGa to the metadata api');
|
|
951
|
+
const previous = regaSync;
|
|
952
|
+
previous.stopPolling();
|
|
953
|
+
previous.removeAllListeners();
|
|
954
|
+
regaSync = createMeta();
|
|
955
|
+
regaSync.load();
|
|
956
|
+
attachProvider();
|
|
957
|
+
regaOk = false;
|
|
958
|
+
await resyncNames();
|
|
959
|
+
scheduleIndex();
|
|
960
|
+
}
|
|
961
|
+
|
|
831
962
|
/** The ReGa's copy of every datapoint value: seeds the value store, published with --publish-cache. */
|
|
832
963
|
async function loadCache() {
|
|
833
964
|
const list = await regaSync.rega.getValues();
|
|
@@ -857,10 +988,12 @@ async function loadCache() {
|
|
|
857
988
|
async function start() {
|
|
858
989
|
adapter.start();
|
|
859
990
|
ccuIp = await resolveCcu();
|
|
860
|
-
if (regaSync) {
|
|
991
|
+
if (regaSync && regaSync.rega) {
|
|
861
992
|
regaSync.rega.host = ccuIp;
|
|
862
993
|
regaSync.rega.url = regaSync.rega.url.replace(host, ccuIp);
|
|
863
994
|
regaSync.rega.webUrl = regaSync.rega.webUrl.replace(host, ccuIp);
|
|
995
|
+
} else if (regaSync) {
|
|
996
|
+
regaSync.setHost(ccuIp);
|
|
864
997
|
}
|
|
865
998
|
enabled =
|
|
866
999
|
parseInterfaces(config.interfaces) || (await probeInterfaces(ccuIp, {tls: config.ccuTls, local: localMode}));
|
package/lib/hadiscovery.js
CHANGED
|
@@ -147,17 +147,29 @@ function deviceBlock({iface, device, list, ctx, t, st, cmd, bridgeId, ignored})
|
|
|
147
147
|
|
|
148
148
|
for (const ch of channels) {
|
|
149
149
|
const consumed = new Set();
|
|
150
|
-
const e = (dp, platform, label, more = {}) =>
|
|
151
|
-
entity({
|
|
150
|
+
const e = (dp, platform, label, more = {}) => {
|
|
151
|
+
const built = entity({
|
|
152
152
|
id,
|
|
153
153
|
name,
|
|
154
|
-
item:
|
|
154
|
+
item: `${ch.index}/${dp}`,
|
|
155
155
|
platform,
|
|
156
156
|
label,
|
|
157
157
|
uid: `${ch.index}_${dp}`,
|
|
158
158
|
jsonPayloads: json,
|
|
159
159
|
...more,
|
|
160
160
|
});
|
|
161
|
+
// The topics are whole, rendered from the configured templates. entity() composes
|
|
162
|
+
// <name>/status|set/<item> for a core-style item, so exactly its composed values are
|
|
163
|
+
// replaced here; an explicit stat_t/cmd_t from `extra` and the stateless platforms
|
|
164
|
+
// (which get none) stay as they are.
|
|
165
|
+
if (built.stat_t === `${name}/status/${ch.index}/${dp}`) {
|
|
166
|
+
built.stat_t = own(dp);
|
|
167
|
+
}
|
|
168
|
+
if (built.cmd_t === `${name}/set/${ch.index}/${dp}`) {
|
|
169
|
+
built.cmd_t = scmd(dp);
|
|
170
|
+
}
|
|
171
|
+
return built;
|
|
172
|
+
};
|
|
161
173
|
const fallback = ch.index === 0 ? 'Maintenance' : `Channel ${ch.index}`;
|
|
162
174
|
const label = labelOf(ch, deviceName, fallback);
|
|
163
175
|
const has = (dp) => Boolean(ch.description[dp]) && !ignored(iface, ch.ADDRESS, dp);
|
package/lib/interfaces.js
CHANGED
|
@@ -180,10 +180,8 @@ export async function detectLocal(host, {timeout = 500, connect = portOpen} = {}
|
|
|
180
180
|
return false;
|
|
181
181
|
}
|
|
182
182
|
const ports = INTERFACE_NAMES.map((name) => INTERFACES[name].localPort).filter(Boolean);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
return false;
|
|
183
|
+
// concurrently: this runs at every startup, and closed-but-filtered ports would otherwise
|
|
184
|
+
// stack their timeouts
|
|
185
|
+
const results = await Promise.all([...ports, REGA_LOCAL_PORT].map((port) => connect(host, port, timeout)));
|
|
186
|
+
return results.some(Boolean);
|
|
189
187
|
}
|