homebridge-withings-environment-data 1.4.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/README.md +22 -16
- package/config.schema.json +24 -9
- package/index.js +70 -15
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## [1.6.1] - 2026-08-21
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Publish workflow's CI Node version bumped to 22 — the first real run
|
|
13
|
+
failed at `npm install -g npm@latest`, since the latest npm now
|
|
14
|
+
requires Node `^22.22.2 || ^24.15.0 || >=26.0.0` and the workflow had
|
|
15
|
+
pinned Node 20
|
|
16
|
+
|
|
17
|
+
## [1.6.0] - 2026-08-21
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- "Requires authentication" checkbox in the MQTT fieldset; Username and
|
|
21
|
+
Password only appear (and are only sent to the broker) when it's checked
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- MQTT Host and Port merged into a single "Host" field including the port,
|
|
25
|
+
e.g. `localhost:1883` (the new default)
|
|
26
|
+
- Publishing to npm now happens automatically via GitHub Actions on tagged
|
|
27
|
+
releases, using npm's OIDC trusted publishing (no npm tokens in CI)
|
|
28
|
+
|
|
29
|
+
## [1.5.0] - 2026-08-19
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
- "Send Stale Data Notification" checkbox (default on): lets the ntfy
|
|
33
|
+
notification for stale data be turned off independently of the log
|
|
34
|
+
warning and "No Response" in the Home app, which still happen either way
|
|
35
|
+
- Log line confirming a successful MQTT broker connection, and one per poll
|
|
36
|
+
reporting how many readings were published and over what time range
|
|
37
|
+
(previously only connection *errors* were logged, so a client that never
|
|
38
|
+
connected looked the same in the log as a working one)
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
- MQTT backfill no longer gets stuck once the newest known reading is
|
|
42
|
+
itself still stale by the time it's fetched (e.g. the scale recovers
|
|
43
|
+
from an outage, but the newest synced point is still older than the
|
|
44
|
+
Stale Data Warning Threshold). Publishing is now gated purely on
|
|
45
|
+
whether a reading has already been sent, not on wall-clock staleness
|
|
46
|
+
- MQTT backfill no longer strands readings on the first run after
|
|
47
|
+
upgrading: the "last published" marker was seeded from the newest
|
|
48
|
+
reading the *poller* had seen rather than the newest actually
|
|
49
|
+
*published*, silently skipping everything in between
|
|
50
|
+
|
|
9
51
|
## [1.4.0] - 2026-08-19
|
|
10
52
|
|
|
11
53
|
### Added
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Withings Environment Data v1.
|
|
1
|
+
# Withings Environment Data v1.6.1
|
|
2
2
|
|
|
3
3
|
**This Homebridge plugin has been 100% vibe coded with Claude.**
|
|
4
4
|
|
|
@@ -62,9 +62,10 @@ turned on or off in Configuration:
|
|
|
62
62
|
readings internally before it syncs, and this backfills that gap instead
|
|
63
63
|
of collapsing it into one message. Each is published in the order it was
|
|
64
64
|
actually recorded, oldest first. Messages are retained by default.
|
|
65
|
-
Publishing
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
Publishing only ever sends readings not already sent before (tracked
|
|
66
|
+
independently of HomeKit's staleness check below), so it keeps working
|
|
67
|
+
even if the newest available reading is itself still old by the time it
|
|
68
|
+
arrives.
|
|
68
69
|
|
|
69
70
|
### Configuration
|
|
70
71
|
|
|
@@ -92,21 +93,26 @@ Fields:
|
|
|
92
93
|
Anything above the Inferior boundary is reported as Poor.
|
|
93
94
|
- **Expose sensors as HomeKit Accessories**: see [Usage](#usage) above.
|
|
94
95
|
Default on.
|
|
95
|
-
- **Publish to MQTT / Host /
|
|
96
|
-
Retain**: see [Usage](#usage) above. Publishing is off by
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
- **Publish to MQTT / Host / Requires authentication / Username / Password /
|
|
97
|
+
Last Seen / Retain**: see [Usage](#usage) above. Publishing is off by
|
|
98
|
+
default. **Host** includes the port, e.g. `localhost:1883` (the
|
|
99
|
+
default). Username/Password only apply, and only appear, when
|
|
100
|
+
**Requires authentication** is checked. **Last Seen** controls the
|
|
101
|
+
format of the `last_seen` field, which always reflects the time the
|
|
102
|
+
scale actually took the measurement (not when the plugin polled or
|
|
103
|
+
published it): `ISO_8601` (default, UTC), `ISO_8601 local` (with UTC
|
|
104
|
+
offset), `epoch` (milliseconds), or `disabled` to omit the field
|
|
105
|
+
entirely. Retain is on by default.
|
|
104
106
|
- **Stale Data Warning Threshold (hours)**: if the newest reading from the
|
|
105
107
|
scale itself (not the plugin's poll) is older than this many hours — e.g.
|
|
106
108
|
nobody's stood on the scale in a while — a warning is logged on every
|
|
107
|
-
poll for as long as it stays stale, the sensors show "No Response" in
|
|
108
|
-
Home app
|
|
109
|
-
|
|
109
|
+
poll for as long as it stays stale, and the sensors show "No Response" in
|
|
110
|
+
the Home app. Default 4. This is separate from poll failures.
|
|
111
|
+
- **Send Stale Data Notification** (default on): whether a stale reading
|
|
112
|
+
additionally sends a single ntfy notification (requires ntfy.sh
|
|
113
|
+
Notification Topic below). The log warning and "No Response" in the Home
|
|
114
|
+
app happen either way, so turn this off if the unresponsive sensor is
|
|
115
|
+
signal enough on its own.
|
|
110
116
|
- **ntfy.sh Notification Topic** (optional): if set, sends a push notification via
|
|
111
117
|
[ntfy.sh](https://ntfy.sh) to this topic the first time a poll fails
|
|
112
118
|
(not repeated on every subsequent failure in the same streak; only once
|
package/config.schema.json
CHANGED
|
@@ -87,13 +87,12 @@
|
|
|
87
87
|
"mqttHost": {
|
|
88
88
|
"title": "Host",
|
|
89
89
|
"type": "string",
|
|
90
|
-
"default": "localhost",
|
|
91
|
-
"description": "Broker hostname or IP address."
|
|
90
|
+
"default": "localhost:1883",
|
|
91
|
+
"description": "Broker hostname or IP address, including port."
|
|
92
92
|
},
|
|
93
|
-
"
|
|
94
|
-
"title": "
|
|
95
|
-
"type": "
|
|
96
|
-
"default": 1883
|
|
93
|
+
"mqttRequiresAuth": {
|
|
94
|
+
"title": "Requires authentication",
|
|
95
|
+
"type": "boolean"
|
|
97
96
|
},
|
|
98
97
|
"mqttUsername": {
|
|
99
98
|
"title": "Username",
|
|
@@ -137,6 +136,11 @@
|
|
|
137
136
|
"minimum": 1,
|
|
138
137
|
"description": "If the newest CO2/temperature reading from the scale itself is older than this many hours, log a warning, show \"No Response\" in the Home app, and send a notification."
|
|
139
138
|
},
|
|
139
|
+
"sendStaleDataNotification": {
|
|
140
|
+
"title": "Send Stale Data Notification",
|
|
141
|
+
"type": "boolean",
|
|
142
|
+
"description": "Send an ntfy notification when data goes stale (requires ntfy.sh Notification Topic below). The log warning and \"No Response\" in the Home app happen either way."
|
|
143
|
+
},
|
|
140
144
|
"ntfyTopic": {
|
|
141
145
|
"title": "ntfy.sh Notification Topic",
|
|
142
146
|
"type": "string",
|
|
@@ -167,9 +171,19 @@
|
|
|
167
171
|
"items": [
|
|
168
172
|
"publishToMqtt",
|
|
169
173
|
"mqttHost",
|
|
170
|
-
"
|
|
171
|
-
|
|
172
|
-
|
|
174
|
+
"mqttRequiresAuth",
|
|
175
|
+
{
|
|
176
|
+
"key": "mqttUsername",
|
|
177
|
+
"condition": {
|
|
178
|
+
"functionBody": "return model.mqttRequiresAuth === true;"
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"key": "mqttPassword",
|
|
183
|
+
"condition": {
|
|
184
|
+
"functionBody": "return model.mqttRequiresAuth === true;"
|
|
185
|
+
}
|
|
186
|
+
},
|
|
173
187
|
"mqttLastSeen",
|
|
174
188
|
"mqttRetain"
|
|
175
189
|
]
|
|
@@ -190,6 +204,7 @@
|
|
|
190
204
|
"pollIntervalMinutes",
|
|
191
205
|
"noResponseAfterMissedPolls",
|
|
192
206
|
"staleDataWarningThresholdHours",
|
|
207
|
+
"sendStaleDataNotification",
|
|
193
208
|
"ntfyTopic"
|
|
194
209
|
]
|
|
195
210
|
}
|
package/index.js
CHANGED
|
@@ -142,11 +142,17 @@ class WithingsEnvironmentDataPlatform {
|
|
|
142
142
|
this.lastReadingDate = state.lastReadingDate ?? null;
|
|
143
143
|
// Unix seconds of the newest reading already published to MQTT, used to
|
|
144
144
|
// backfill any points buffered by the scale since the last publish rather
|
|
145
|
-
// than only ever sending the single newest one.
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
145
|
+
// than only ever sending the single newest one.
|
|
146
|
+
//
|
|
147
|
+
// Deliberately NOT seeded from lastReadingDate when absent: that tracks the
|
|
148
|
+
// newest reading the *poller* has seen, which is not the same as the newest
|
|
149
|
+
// reading actually *published*. Seeding from it silently strands everything
|
|
150
|
+
// in between (found live: ~12h of readings were skipped on upgrade because
|
|
151
|
+
// lastReadingDate had moved on while MQTT publishing was blocked). Starting
|
|
152
|
+
// at null instead republishes at most one window on first run, which the
|
|
153
|
+
// watermark then dedups from — failing toward a duplicate rather than
|
|
154
|
+
// toward permanent data loss.
|
|
155
|
+
this.lastPublishedMqttDate = state.lastPublishedMqttDate ?? null;
|
|
150
156
|
// The readingDate (unix seconds) we last sent a stale-data *notification* for,
|
|
151
157
|
// or null if not currently in a notified state. The log warning itself repeats
|
|
152
158
|
// every poll while data stays stale (like missed-poll-cycle failures do), but
|
|
@@ -250,21 +256,43 @@ class WithingsEnvironmentDataPlatform {
|
|
|
250
256
|
return this.config.publishToMqtt === true;
|
|
251
257
|
}
|
|
252
258
|
|
|
259
|
+
// mqttHost is a single "host[:port]" field; older configs saved before that
|
|
260
|
+
// merge may still have a separate mqttPort value sitting in config.json
|
|
261
|
+
// (config UI stops writing it once resaved, but doesn't retroactively
|
|
262
|
+
// delete it), so fall back to that before the hardcoded default.
|
|
263
|
+
getMqttHostAndPort() {
|
|
264
|
+
const raw = this.config.mqttHost || '';
|
|
265
|
+
const colonIndex = raw.lastIndexOf(':');
|
|
266
|
+
if (colonIndex > -1) {
|
|
267
|
+
const host = raw.slice(0, colonIndex);
|
|
268
|
+
const port = Number(raw.slice(colonIndex + 1));
|
|
269
|
+
if (host && Number.isFinite(port) && port > 0) {
|
|
270
|
+
return { host, port };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const legacyPort = Number(this.config.mqttPort);
|
|
274
|
+
return { host: raw, port: Number.isFinite(legacyPort) && legacyPort > 0 ? legacyPort : 1883 };
|
|
275
|
+
}
|
|
276
|
+
|
|
253
277
|
connectMqtt() {
|
|
254
278
|
if (!this.getPublishToMqtt()) return;
|
|
255
279
|
|
|
256
|
-
|
|
280
|
+
const { host, port } = this.getMqttHostAndPort();
|
|
281
|
+
if (!host) {
|
|
257
282
|
this.log.warn('Publish to MQTT is enabled but no MQTT Host is configured; MQTT publishing is disabled.');
|
|
258
283
|
return;
|
|
259
284
|
}
|
|
260
285
|
|
|
261
|
-
const
|
|
262
|
-
const
|
|
286
|
+
const url = `mqtt://${host}:${port}`;
|
|
287
|
+
const requiresAuth = this.config.mqttRequiresAuth === true;
|
|
263
288
|
this.mqttClient = mqtt.connect(url, {
|
|
264
|
-
username: this.config.mqttUsername || undefined,
|
|
265
|
-
password: this.config.mqttPassword || undefined,
|
|
289
|
+
username: requiresAuth ? this.config.mqttUsername || undefined : undefined,
|
|
290
|
+
password: requiresAuth ? this.config.mqttPassword || undefined : undefined,
|
|
266
291
|
});
|
|
267
292
|
this.mqttClient.on('error', (err) => this.log.warn(`MQTT connection error: ${err.message}`));
|
|
293
|
+
// Without this, a silently-never-connecting client looks identical in the
|
|
294
|
+
// log to a working one, since only the error path said anything.
|
|
295
|
+
this.mqttClient.on('connect', () => this.log.info(`Connected to MQTT broker at ${url}.`));
|
|
268
296
|
if (this.config.mqttRetain === false) {
|
|
269
297
|
// A publish with retain:false does NOT clear a previously-retained message —
|
|
270
298
|
// the broker keeps serving the last retained one until something explicitly
|
|
@@ -304,9 +332,17 @@ class WithingsEnvironmentDataPlatform {
|
|
|
304
332
|
// synced a backlog of readings (e.g. after being offline) gets backfilled on
|
|
305
333
|
// the MQTT side rather than collapsed into one message. HomeKit is
|
|
306
334
|
// unaffected: it only ever shows the single newest value (see applyReading).
|
|
335
|
+
//
|
|
336
|
+
// Deliberately NOT gated on isDataStale(): that reflects whether the newest
|
|
337
|
+
// known reading is old relative to *now*, which can still be true right
|
|
338
|
+
// after a real recovery (e.g. the scale's own buffered backlog syncs, but
|
|
339
|
+
// its newest point is still older than the threshold). Gating on that would
|
|
340
|
+
// strand genuinely new, never-before-published backlog until an even
|
|
341
|
+
// fresher point arrives later. Dedup below (lastPublishedMqttDate) already
|
|
342
|
+
// prevents ever republishing the same reading, which is the only thing this
|
|
343
|
+
// gate needs to guard against.
|
|
307
344
|
publishMqttReadings(co2Series, tempSeries) {
|
|
308
345
|
if (!this.mqttClient) return;
|
|
309
|
-
if (this.isDataStale()) return;
|
|
310
346
|
|
|
311
347
|
const byDate = new Map();
|
|
312
348
|
for (const point of co2Series) {
|
|
@@ -335,6 +371,17 @@ class WithingsEnvironmentDataPlatform {
|
|
|
335
371
|
|
|
336
372
|
if (unpublished.length > 0) {
|
|
337
373
|
this.persistState();
|
|
374
|
+
const first = this.formatStaleDateTime(unpublished[0][0]);
|
|
375
|
+
const last = this.formatStaleDateTime(unpublished[unpublished.length - 1][0]);
|
|
376
|
+
const range =
|
|
377
|
+
unpublished.length === 1
|
|
378
|
+
? `${first.date} ${first.time}`
|
|
379
|
+
: `${first.date} ${first.time} to ${last.date} ${last.time}`;
|
|
380
|
+
this.log.info(`Published ${unpublished.length} reading(s) to MQTT (${range}).`);
|
|
381
|
+
} else {
|
|
382
|
+
// Distinguishes "nothing new to send" from "never connected/never tried",
|
|
383
|
+
// which otherwise look the same from the log alone.
|
|
384
|
+
this.log.debug('No new readings to publish to MQTT; nothing newer than the last published reading.');
|
|
338
385
|
}
|
|
339
386
|
}
|
|
340
387
|
|
|
@@ -486,6 +533,12 @@ class WithingsEnvironmentDataPlatform {
|
|
|
486
533
|
};
|
|
487
534
|
}
|
|
488
535
|
|
|
536
|
+
// Same Config UI boolean-default rendering quirk as mqttRetain (see
|
|
537
|
+
// getMqttRetain), so "on by default" is a runtime default, not a schema one.
|
|
538
|
+
getSendStaleDataNotification() {
|
|
539
|
+
return this.config.sendStaleDataNotification !== false;
|
|
540
|
+
}
|
|
541
|
+
|
|
489
542
|
getStaleDataThresholdHours() {
|
|
490
543
|
const threshold = Number(this.config.staleDataWarningThresholdHours);
|
|
491
544
|
return Number.isFinite(threshold) && threshold >= 1 ? threshold : 4;
|
|
@@ -519,10 +572,12 @@ class WithingsEnvironmentDataPlatform {
|
|
|
519
572
|
if (!alreadyNotifiedForThisReading) {
|
|
520
573
|
this.staleNotifiedForReadingDate = this.lastReadingDate;
|
|
521
574
|
this.persistState();
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
575
|
+
if (this.getSendStaleDataNotification()) {
|
|
576
|
+
await this.sendNtfyNotification(
|
|
577
|
+
`Temperature and/or CO2 readings haven't been updated since ${date} at ${time}.`,
|
|
578
|
+
'Homebridge: Withings Environment Data is out of date'
|
|
579
|
+
);
|
|
580
|
+
}
|
|
526
581
|
}
|
|
527
582
|
} else if (this.staleNotifiedForReadingDate !== null) {
|
|
528
583
|
this.staleNotifiedForReadingDate = null;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-withings-environment-data",
|
|
3
3
|
"displayName": "Withings Environment Data",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.6.1",
|
|
5
5
|
"description": "Homebridge plugin exposing ambient CO2/air-quality and room temperature readings from a Withings WS-50 scale as HomeKit sensors",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"files": [
|