homebridge-navilink 0.1.2 → 0.1.4
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 +14 -0
- package/config.schema.json +3 -0
- package/dist/diagnostics/format.d.ts +1 -1
- package/dist/diagnostics/format.js +3 -2
- package/dist/platform.js +1 -1
- package/dist/session.d.ts +1 -1
- package/dist/session.js +4 -4
- package/dist/utils/labels.d.ts +2 -0
- package/dist/utils/labels.js +3 -0
- package/docs/FEATURES.md +1 -1
- package/docs/README-DETAILED.md +6 -6
- package/homebridge-ui/public/index.html +12 -12
- package/homebridge-ui/public/index.js +27 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.4](https://github.com/tbaur/homebridge-navilink/compare/v0.1.3...v0.1.4) (2026-09-20)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* align the diagnostics slider and keep undiscovered hardware off ([#10](https://github.com/tbaur/homebridge-navilink/issues/10)) ([298a06c](https://github.com/tbaur/homebridge-navilink/commit/298a06cdd2f0835b829dd3f2b8b02595f28f9224))
|
|
9
|
+
|
|
10
|
+
## [0.1.3](https://github.com/tbaur/homebridge-navilink/compare/v0.1.2...v0.1.3) (2026-09-20)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* use a diagnostics slider and name the mqtt channel ([#8](https://github.com/tbaur/homebridge-navilink/issues/8)) ([324d700](https://github.com/tbaur/homebridge-navilink/commit/324d7006dda002ad0469599012c67222558aae53))
|
|
16
|
+
|
|
3
17
|
## [0.1.2](https://github.com/tbaur/homebridge-navilink/compare/v0.1.1...v0.1.2) (2026-09-20)
|
|
4
18
|
|
|
5
19
|
|
package/config.schema.json
CHANGED
|
@@ -60,6 +60,9 @@
|
|
|
60
60
|
"default": 0,
|
|
61
61
|
"minimum": 0,
|
|
62
62
|
"maximum": 3600,
|
|
63
|
+
"x-schema-form": {
|
|
64
|
+
"type": "number"
|
|
65
|
+
},
|
|
63
66
|
"description": "Emit a plugin health line to the Homebridge log every N seconds. 0 is off (the default). Allowed range 0–3600; values between 1 and 29 are clamped up to 30. Logs only; nothing is exposed in HomeKit."
|
|
64
67
|
},
|
|
65
68
|
"structuredLogs": {
|
|
@@ -15,7 +15,7 @@ export declare function diagnosticLabel(msg: string): string;
|
|
|
15
15
|
export declare function formatReasons(reasons: string[]): string;
|
|
16
16
|
/**
|
|
17
17
|
* Concise summary matching the sibling plugins:
|
|
18
|
-
* `Health: healthy | devices 1/1 | mqtt live | api p50 12ms p95 40ms (req 3, err 0)`.
|
|
18
|
+
* `Health: healthy | devices 1/1 | Publish-subscribe (mqtt) live | api p50 12ms p95 40ms (req 3, err 0)`.
|
|
19
19
|
*/
|
|
20
20
|
export declare function formatDiagnosticLine(report: DiagnosticsSnapshot): string;
|
|
21
21
|
/** State-only line for a healthy/degraded flip. The heartbeat already has the body. */
|
|
@@ -13,6 +13,7 @@ exports.diagnosticLabel = diagnosticLabel;
|
|
|
13
13
|
exports.formatReasons = formatReasons;
|
|
14
14
|
exports.formatDiagnosticLine = formatDiagnosticLine;
|
|
15
15
|
exports.formatHealthTransitionLine = formatHealthTransitionLine;
|
|
16
|
+
const labels_1 = require("../utils/labels");
|
|
16
17
|
/** Short operator-facing label for the MQTT session lifecycle. */
|
|
17
18
|
function formatMqttTransportState(state) {
|
|
18
19
|
switch (state) {
|
|
@@ -52,14 +53,14 @@ function formatReasons(reasons) {
|
|
|
52
53
|
}
|
|
53
54
|
/**
|
|
54
55
|
* Concise summary matching the sibling plugins:
|
|
55
|
-
* `Health: healthy | devices 1/1 | mqtt live | api p50 12ms p95 40ms (req 3, err 0)`.
|
|
56
|
+
* `Health: healthy | devices 1/1 | Publish-subscribe (mqtt) live | api p50 12ms p95 40ms (req 3, err 0)`.
|
|
56
57
|
*/
|
|
57
58
|
function formatDiagnosticLine(report) {
|
|
58
59
|
const { lifecycle, devices, transport, api } = report;
|
|
59
60
|
return [
|
|
60
61
|
`${diagnosticLabel(report.msg)}: ${lifecycle.health}${formatReasons(lifecycle.reasons)}`,
|
|
61
62
|
`devices ${devices.online}/${devices.total}`,
|
|
62
|
-
|
|
63
|
+
`${labels_1.MQTT_CHANNEL} ${formatMqttTransportState(transport.mqttState)}`,
|
|
63
64
|
`api p50 ${api.p50Ms}ms p95 ${api.p95Ms}ms (req ${api.requests}, err ${api.errors})`,
|
|
64
65
|
].join(' | ');
|
|
65
66
|
}
|
package/dist/platform.js
CHANGED
|
@@ -347,7 +347,7 @@ class NaviLinkPlatform {
|
|
|
347
347
|
}
|
|
348
348
|
this.cloudOffline = false;
|
|
349
349
|
this.lastOutageWarnAt = 0;
|
|
350
|
-
this.log.info(
|
|
350
|
+
this.log.info(`${utils_1.MQTT_CHANNEL} recovered`);
|
|
351
351
|
}
|
|
352
352
|
// --- AccessoryHost ----------------------------------------------------------
|
|
353
353
|
deviceFor(deviceId) {
|
package/dist/session.d.ts
CHANGED
|
@@ -107,7 +107,7 @@ export declare class NaviLinkSession {
|
|
|
107
107
|
private readonly announcedFamilies;
|
|
108
108
|
/** Firmware lines already announced at info, so a credential refresh is not a new event. */
|
|
109
109
|
private readonly announcedFirmware;
|
|
110
|
-
/** True after the first `mqtt up` line, so a refresh is not a boot. */
|
|
110
|
+
/** True after the first `Publish-subscribe (mqtt) up` line, so a refresh is not a boot. */
|
|
111
111
|
private liveAnnounced;
|
|
112
112
|
/** True after an unexpected drop, so the next connect is a recovery. */
|
|
113
113
|
private liveWasDown;
|
package/dist/session.js
CHANGED
|
@@ -80,7 +80,7 @@ class NaviLinkSession {
|
|
|
80
80
|
announcedFamilies = new Set();
|
|
81
81
|
/** Firmware lines already announced at info, so a credential refresh is not a new event. */
|
|
82
82
|
announcedFirmware = new Set();
|
|
83
|
-
/** True after the first `mqtt up` line, so a refresh is not a boot. */
|
|
83
|
+
/** True after the first `Publish-subscribe (mqtt) up` line, so a refresh is not a boot. */
|
|
84
84
|
liveAnnounced = false;
|
|
85
85
|
/** True after an unexpected drop, so the next connect is a recovery. */
|
|
86
86
|
liveWasDown = false;
|
|
@@ -416,19 +416,19 @@ class NaviLinkSession {
|
|
|
416
416
|
// A refused fallback must not look like an outage mid-establish.
|
|
417
417
|
connection.onClose((error) => this.handleConnectionClosed(connection, error));
|
|
418
418
|
if (!this.liveAnnounced || this.liveWasDown) {
|
|
419
|
-
this.log.info(
|
|
419
|
+
this.log.info(`${utils_1.MQTT_CHANNEL} up`);
|
|
420
420
|
this.liveAnnounced = true;
|
|
421
421
|
this.liveWasDown = false;
|
|
422
422
|
}
|
|
423
423
|
else {
|
|
424
|
-
this.log.debug(
|
|
424
|
+
this.log.debug(`${utils_1.MQTT_CHANNEL} up`);
|
|
425
425
|
}
|
|
426
426
|
return;
|
|
427
427
|
}
|
|
428
428
|
catch (error) {
|
|
429
429
|
lastError = error;
|
|
430
430
|
connection.close();
|
|
431
|
-
this.log.debug(
|
|
431
|
+
this.log.debug(`${utils_1.MQTT_CHANNEL} connect failed with host${signHostWithPort ? ':443' : ''} signature: `
|
|
432
432
|
+ (0, utils_1.describeError)(error));
|
|
433
433
|
}
|
|
434
434
|
}
|
package/dist/utils/labels.d.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* with. Use the name the user gave the appliance. Fall back to a masked
|
|
12
12
|
* gateway only when there is no name, and say that it is a gateway.
|
|
13
13
|
*/
|
|
14
|
+
/** Operator-facing name for the MQTT live channel. */
|
|
15
|
+
export declare const MQTT_CHANNEL = "Publish-subscribe (mqtt)";
|
|
14
16
|
/** The appliance name, or `gateway …ABCD` when none is known. */
|
|
15
17
|
export declare function labelAppliance(input: {
|
|
16
18
|
name?: string;
|
package/dist/utils/labels.js
CHANGED
|
@@ -13,9 +13,12 @@
|
|
|
13
13
|
* gateway only when there is no name, and say that it is a gateway.
|
|
14
14
|
*/
|
|
15
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.MQTT_CHANNEL = void 0;
|
|
16
17
|
exports.labelAppliance = labelAppliance;
|
|
17
18
|
const validators_1 = require("./validators");
|
|
18
19
|
const redact_1 = require("./redact");
|
|
20
|
+
/** Operator-facing name for the MQTT live channel. */
|
|
21
|
+
exports.MQTT_CHANNEL = 'Publish-subscribe (mqtt)';
|
|
19
22
|
/** The appliance name, or `gateway …ABCD` when none is known. */
|
|
20
23
|
function labelAppliance(input) {
|
|
21
24
|
const name = input.name?.trim();
|
package/docs/FEATURES.md
CHANGED
|
@@ -35,7 +35,7 @@ A checklist of what is built. The plugin aims to cover everything about a Navien
|
|
|
35
35
|
- ✅ Read-only mode: every accessory reports state, and no control command is ever sent (`readOnly; write ignored`)
|
|
36
36
|
- ✅ Power-off guard, on by default, because a combi's power state governs central heating as well as hot water (`power-off disabled (allowPowerOff is off)`)
|
|
37
37
|
- ✅ Accessory name prefix (`options.accessoryPrefix`): one editable stem for every HomeKit tile, without changing accessory identity
|
|
38
|
-
- ✅ Opt-in diagnostics (`options.diagnosticsInterval`, default 0 / off): a periodic heartbeat (`Health: healthy | devices n/n | mqtt live | api p50/p95 (req, err)`), with optional structured JSON (`options.structuredLogs`)
|
|
38
|
+
- ✅ Opt-in diagnostics (`options.diagnosticsInterval`, default 0 / off): a periodic heartbeat (`Health: healthy | devices n/n | Publish-subscribe (mqtt) live | api p50/p95 (req, err)`), with optional structured JSON (`options.structuredLogs`)
|
|
39
39
|
- ✅ Secrets redacted from every log line by shape, not by field name, so an unfamiliar response cannot leak a token
|
|
40
40
|
- ✅ HomeKit serial numbers are opaque generated values, never the gateway MAC
|
|
41
41
|
- ✅ Bounded I/O: separate connect and total timeouts, a capped response size, and a capped MQTT packet size
|
package/docs/README-DETAILED.md
CHANGED
|
@@ -106,7 +106,7 @@ An appliance with no probe reports `0`. The plugin treats that as absent, not as
|
|
|
106
106
|
|
|
107
107
|
**Control is rate-limited, and the cloud's own limit is honoured.** Commands to one gateway are spaced out. When the cloud refuses one for arriving too soon, the log is `rate limited` and the plugin pauses for 30 seconds. It does not keep sending to a channel that is already refusing.
|
|
108
108
|
|
|
109
|
-
**Backoff has jitter.** Reconnects use exponential backoff with full jitter to a capped ceiling, so a regional outage does not produce a synchronised stampede when it ends. A real drop logs the close, then `reconnect in Ns`, then `mqtt recovered` when the session is up again.
|
|
109
|
+
**Backoff has jitter.** Reconnects use exponential backoff with full jitter to a capped ceiling, so a regional outage does not produce a synchronised stampede when it ends. A real drop logs the close, then `reconnect in Ns`, then `Publish-subscribe (mqtt) recovered` when the session is up again.
|
|
110
110
|
|
|
111
111
|
## Full configuration reference
|
|
112
112
|
|
|
@@ -198,7 +198,7 @@ Startup, in order:
|
|
|
198
198
|
adding Boiler Hot Water
|
|
199
199
|
adding Boiler Heating
|
|
200
200
|
Boiler firmware 4352
|
|
201
|
-
mqtt up
|
|
201
|
+
Publish-subscribe (mqtt) up
|
|
202
202
|
Boiler channel 1 family=NCB
|
|
203
203
|
1 appliance(s), 8 accessory(ies)
|
|
204
204
|
```
|
|
@@ -228,13 +228,13 @@ An outage, and its recovery. Every outage warns on the way in so that it gets a
|
|
|
228
228
|
```
|
|
229
229
|
NaviLink broker closed the connection (code 1006)
|
|
230
230
|
reconnect in 2s
|
|
231
|
-
mqtt up
|
|
232
|
-
mqtt recovered
|
|
231
|
+
Publish-subscribe (mqtt) up
|
|
232
|
+
Publish-subscribe (mqtt) recovered
|
|
233
233
|
```
|
|
234
234
|
|
|
235
235
|
A continuing outage repeats at most hourly, at debug in between.
|
|
236
236
|
|
|
237
|
-
A credential refresh is not an outage. The plugin closes the socket itself a few minutes before the AWS credentials expire, signs in again, and the tiles stay current. That is debug only. `family=`, firmware and `mqtt up` do not repeat at info.
|
|
237
|
+
A credential refresh is not an outage. The plugin closes the socket itself a few minutes before the AWS credentials expire, signs in again, and the tiles stay current. That is debug only. `family=`, firmware and `Publish-subscribe (mqtt) up` do not repeat at info.
|
|
238
238
|
|
|
239
239
|
### Diagnostics (optional)
|
|
240
240
|
|
|
@@ -243,7 +243,7 @@ Set `options.diagnosticsInterval` to a value between `30` and `3600` seconds to
|
|
|
243
243
|
It pairs with `options.structuredLogs: true`, which adds a JSON line next to the human one.
|
|
244
244
|
|
|
245
245
|
```
|
|
246
|
-
Health: healthy | devices 1/1 | mqtt live | api p50 80ms p95 120ms (req 3, err 0)
|
|
246
|
+
Health: healthy | devices 1/1 | Publish-subscribe (mqtt) live | api p50 80ms p95 120ms (req 3, err 0)
|
|
247
247
|
```
|
|
248
248
|
|
|
249
249
|
| `msg` | Level | When |
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
.nl-toolbar { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; margin-bottom: 1rem; }
|
|
29
29
|
.nl-toolbar .spacer { flex: 1 1 auto; }
|
|
30
30
|
.nl-guard { border-left: 3px solid rgba(255, 193, 7, .8); padding-left: .7rem; margin-top: .6rem; }
|
|
31
|
+
.nl-slider { display: block; }
|
|
32
|
+
.nl-slider-head { display: flex; justify-content: space-between; align-items: baseline; gap: .75rem; }
|
|
33
|
+
.nl-slider-head label { font-size: .8rem; margin: 0; }
|
|
34
|
+
.nl-slider-head span { font-size: .8rem; font-variant-numeric: tabular-nums; flex: 0 0 auto; }
|
|
35
|
+
.nl-slider input[type="range"] { display: block; width: 100%; margin: .35rem 0 0; }
|
|
31
36
|
</style>
|
|
32
37
|
|
|
33
38
|
<div class="nl-intro">
|
|
@@ -123,19 +128,14 @@
|
|
|
123
128
|
</div>
|
|
124
129
|
</div>
|
|
125
130
|
|
|
126
|
-
<div class="form-group mt-3 mb-2">
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
<option value="300">Every 5 minutes</option>
|
|
133
|
-
<option value="900">Every 15 minutes</option>
|
|
134
|
-
<option value="1800">Every 30 minutes</option>
|
|
135
|
-
<option value="3600">Every 60 minutes</option>
|
|
136
|
-
</select>
|
|
131
|
+
<div class="form-group nl-slider mt-3 mb-2">
|
|
132
|
+
<div class="nl-slider-head">
|
|
133
|
+
<label for="diagnostics-interval">Diagnostics interval (seconds)</label>
|
|
134
|
+
<span id="diagnostics-interval-value">0</span>
|
|
135
|
+
</div>
|
|
136
|
+
<input type="range" id="diagnostics-interval" min="0" max="3600" step="1" value="0" />
|
|
137
137
|
<div class="nl-meta">
|
|
138
|
-
A health line in the Homebridge log.
|
|
138
|
+
A health line in the Homebridge log. 0 is off. Nothing is exposed in HomeKit.
|
|
139
139
|
</div>
|
|
140
140
|
</div>
|
|
141
141
|
<div class="nl-options">
|
|
@@ -140,10 +140,17 @@
|
|
|
140
140
|
family: '',
|
|
141
141
|
model: '',
|
|
142
142
|
firmware: '',
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
143
|
+
// Until a sign-in describes the appliance, only offer hardware the
|
|
144
|
+
// saved configuration already has. Recirculation and an outdoor probe
|
|
145
|
+
// are fitted or they are not; turning them on here without asking the
|
|
146
|
+
// gateway would invent a tile that cannot work. Sign in again after
|
|
147
|
+
// new hardware is commissioned.
|
|
148
|
+
capabilities: {
|
|
149
|
+
dhw: device.dhw !== false,
|
|
150
|
+
heating: device.heating === true,
|
|
151
|
+
recirculation: device.recirculation === true,
|
|
152
|
+
outdoorSensor: device.outdoorSensor === true,
|
|
153
|
+
},
|
|
147
154
|
known: false,
|
|
148
155
|
online: undefined,
|
|
149
156
|
missing: false,
|
|
@@ -247,14 +254,17 @@
|
|
|
247
254
|
? platformConfig.options.accessoryPrefix
|
|
248
255
|
: ''
|
|
249
256
|
const diagnostics = Number(platformConfig.options.diagnosticsInterval) || 0
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
diagnosticsSelect.append(el('option', { value: String(diagnostics) }, `Every ${diagnostics} seconds`))
|
|
253
|
-
}
|
|
254
|
-
diagnosticsSelect.value = String(diagnostics)
|
|
257
|
+
byId('diagnostics-interval').value = String(diagnostics)
|
|
258
|
+
syncDiagnosticsLabel()
|
|
255
259
|
byId('structured-logs').checked = platformConfig.options.structuredLogs === true
|
|
256
260
|
}
|
|
257
261
|
|
|
262
|
+
/** Keep the slider's number in step with the thumb. 0 is Off. */
|
|
263
|
+
function syncDiagnosticsLabel() {
|
|
264
|
+
const seconds = Number(byId('diagnostics-interval').value) || 0
|
|
265
|
+
byId('diagnostics-interval-value').textContent = seconds === 0 ? 'Off' : String(seconds)
|
|
266
|
+
}
|
|
267
|
+
|
|
258
268
|
/**
|
|
259
269
|
* Fold the whole-install settings back into the configuration.
|
|
260
270
|
*
|
|
@@ -501,7 +511,14 @@
|
|
|
501
511
|
byId('accessory-prefix').addEventListener('input', () => changed())
|
|
502
512
|
byId('read-only').addEventListener('change', () => changed())
|
|
503
513
|
byId('allow-power-off').addEventListener('change', () => changed())
|
|
504
|
-
byId('diagnostics-interval').addEventListener('
|
|
514
|
+
byId('diagnostics-interval').addEventListener('input', () => {
|
|
515
|
+
syncDiagnosticsLabel()
|
|
516
|
+
changed()
|
|
517
|
+
})
|
|
518
|
+
byId('diagnostics-interval').addEventListener('change', () => {
|
|
519
|
+
syncDiagnosticsLabel()
|
|
520
|
+
changed()
|
|
521
|
+
})
|
|
505
522
|
byId('structured-logs').addEventListener('change', () => changed())
|
|
506
523
|
byId('toggle-json').addEventListener('click', () => {
|
|
507
524
|
schemaFormVisible = !schemaFormVisible
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-navilink",
|
|
3
3
|
"displayName": "Homebridge NaviLink",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.4",
|
|
5
5
|
"description": "Homebridge plugin for Navien combi boilers and water heaters through the North American NaviLink cloud: hot water and space-heating thermostats, power, recirculation, fault and temperature accessories. Verified on an NCB-240E (firmware 4352)",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|