homebridge-withings-environment-data 1.4.0 → 1.5.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 CHANGED
@@ -6,6 +6,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## [1.5.0] - 2026-08-19
10
+
11
+ ### Added
12
+ - "Send Stale Data Notification" checkbox (default on): lets the ntfy
13
+ notification for stale data be turned off independently of the log
14
+ warning and "No Response" in the Home app, which still happen either way
15
+ - Log line confirming a successful MQTT broker connection, and one per poll
16
+ reporting how many readings were published and over what time range
17
+ (previously only connection *errors* were logged, so a client that never
18
+ connected looked the same in the log as a working one)
19
+
20
+ ### Fixed
21
+ - MQTT backfill no longer gets stuck once the newest known reading is
22
+ itself still stale by the time it's fetched (e.g. the scale recovers
23
+ from an outage, but the newest synced point is still older than the
24
+ Stale Data Warning Threshold). Publishing is now gated purely on
25
+ whether a reading has already been sent, not on wall-clock staleness
26
+ - MQTT backfill no longer strands readings on the first run after
27
+ upgrading: the "last published" marker was seeded from the newest
28
+ reading the *poller* had seen rather than the newest actually
29
+ *published*, silently skipping everything in between
30
+
9
31
  ## [1.4.0] - 2026-08-19
10
32
 
11
33
  ### Added
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Withings Environment Data v1.4.0
1
+ # Withings Environment Data v1.5.0
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 pauses entirely once the newest known reading is stale (see
66
- Stale Data Warning Threshold below) and resumes (with any accumulated
67
- backlog) once a fresh reading comes in.
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
 
@@ -104,9 +105,13 @@ Fields:
104
105
  - **Stale Data Warning Threshold (hours)**: if the newest reading from the
105
106
  scale itself (not the plugin's poll) is older than this many hours — e.g.
106
107
  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 the
108
- Home app, and (if ntfy Topic is set) a single notification is sent for
109
- that stale reading. Default 4. This is separate from poll failures.
108
+ poll for as long as it stays stale, and the sensors show "No Response" in
109
+ the Home app. Default 4. This is separate from poll failures.
110
+ - **Send Stale Data Notification** (default on): whether a stale reading
111
+ additionally sends a single ntfy notification (requires ntfy.sh
112
+ Notification Topic below). The log warning and "No Response" in the Home
113
+ app happen either way, so turn this off if the unresponsive sensor is
114
+ signal enough on its own.
110
115
  - **ntfy.sh Notification Topic** (optional): if set, sends a push notification via
111
116
  [ntfy.sh](https://ntfy.sh) to this topic the first time a poll fails
112
117
  (not repeated on every subsequent failure in the same streak; only once
@@ -137,6 +137,11 @@
137
137
  "minimum": 1,
138
138
  "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
139
  },
140
+ "sendStaleDataNotification": {
141
+ "title": "Send Stale Data Notification",
142
+ "type": "boolean",
143
+ "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."
144
+ },
140
145
  "ntfyTopic": {
141
146
  "title": "ntfy.sh Notification Topic",
142
147
  "type": "string",
@@ -190,6 +195,7 @@
190
195
  "pollIntervalMinutes",
191
196
  "noResponseAfterMissedPolls",
192
197
  "staleDataWarningThresholdHours",
198
+ "sendStaleDataNotification",
193
199
  "ntfyTopic"
194
200
  ]
195
201
  }
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. Defaults to the already-
146
- // known lastReadingDate (if any) on first boot after upgrading to this
147
- // feature, so existing installs don't replay their entire pre-existing
148
- // history — only genuinely new backlog from here on.
149
- this.lastPublishedMqttDate = state.lastPublishedMqttDate ?? state.lastReadingDate ?? null;
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
@@ -265,6 +271,9 @@ class WithingsEnvironmentDataPlatform {
265
271
  password: this.config.mqttPassword || undefined,
266
272
  });
267
273
  this.mqttClient.on('error', (err) => this.log.warn(`MQTT connection error: ${err.message}`));
274
+ // Without this, a silently-never-connecting client looks identical in the
275
+ // log to a working one, since only the error path said anything.
276
+ this.mqttClient.on('connect', () => this.log.info(`Connected to MQTT broker at ${url}.`));
268
277
  if (this.config.mqttRetain === false) {
269
278
  // A publish with retain:false does NOT clear a previously-retained message —
270
279
  // the broker keeps serving the last retained one until something explicitly
@@ -304,9 +313,17 @@ class WithingsEnvironmentDataPlatform {
304
313
  // synced a backlog of readings (e.g. after being offline) gets backfilled on
305
314
  // the MQTT side rather than collapsed into one message. HomeKit is
306
315
  // unaffected: it only ever shows the single newest value (see applyReading).
316
+ //
317
+ // Deliberately NOT gated on isDataStale(): that reflects whether the newest
318
+ // known reading is old relative to *now*, which can still be true right
319
+ // after a real recovery (e.g. the scale's own buffered backlog syncs, but
320
+ // its newest point is still older than the threshold). Gating on that would
321
+ // strand genuinely new, never-before-published backlog until an even
322
+ // fresher point arrives later. Dedup below (lastPublishedMqttDate) already
323
+ // prevents ever republishing the same reading, which is the only thing this
324
+ // gate needs to guard against.
307
325
  publishMqttReadings(co2Series, tempSeries) {
308
326
  if (!this.mqttClient) return;
309
- if (this.isDataStale()) return;
310
327
 
311
328
  const byDate = new Map();
312
329
  for (const point of co2Series) {
@@ -335,6 +352,17 @@ class WithingsEnvironmentDataPlatform {
335
352
 
336
353
  if (unpublished.length > 0) {
337
354
  this.persistState();
355
+ const first = this.formatStaleDateTime(unpublished[0][0]);
356
+ const last = this.formatStaleDateTime(unpublished[unpublished.length - 1][0]);
357
+ const range =
358
+ unpublished.length === 1
359
+ ? `${first.date} ${first.time}`
360
+ : `${first.date} ${first.time} to ${last.date} ${last.time}`;
361
+ this.log.info(`Published ${unpublished.length} reading(s) to MQTT (${range}).`);
362
+ } else {
363
+ // Distinguishes "nothing new to send" from "never connected/never tried",
364
+ // which otherwise look the same from the log alone.
365
+ this.log.debug('No new readings to publish to MQTT; nothing newer than the last published reading.');
338
366
  }
339
367
  }
340
368
 
@@ -486,6 +514,12 @@ class WithingsEnvironmentDataPlatform {
486
514
  };
487
515
  }
488
516
 
517
+ // Same Config UI boolean-default rendering quirk as mqttRetain (see
518
+ // getMqttRetain), so "on by default" is a runtime default, not a schema one.
519
+ getSendStaleDataNotification() {
520
+ return this.config.sendStaleDataNotification !== false;
521
+ }
522
+
489
523
  getStaleDataThresholdHours() {
490
524
  const threshold = Number(this.config.staleDataWarningThresholdHours);
491
525
  return Number.isFinite(threshold) && threshold >= 1 ? threshold : 4;
@@ -519,10 +553,12 @@ class WithingsEnvironmentDataPlatform {
519
553
  if (!alreadyNotifiedForThisReading) {
520
554
  this.staleNotifiedForReadingDate = this.lastReadingDate;
521
555
  this.persistState();
522
- await this.sendNtfyNotification(
523
- `Temperature and/or CO2 readings haven't been updated since ${date} at ${time}.`,
524
- 'Homebridge: Withings Environment Data is out of date'
525
- );
556
+ if (this.getSendStaleDataNotification()) {
557
+ await this.sendNtfyNotification(
558
+ `Temperature and/or CO2 readings haven't been updated since ${date} at ${time}.`,
559
+ 'Homebridge: Withings Environment Data is out of date'
560
+ );
561
+ }
526
562
  }
527
563
  } else if (this.staleNotifiedForReadingDate !== null) {
528
564
  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.0",
4
+ "version": "1.5.0",
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": [