homebridge-withings-environment-data 1.0.0 → 1.1.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,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## [1.1.0] - 2026-07-27
10
+
11
+ ### Added
12
+ - Air Quality threshold config fields (`airQualityExcellentMaxPpm`,
13
+ `airQualityGoodMaxPpm`, `airQualityFairMaxPpm`,
14
+ `airQualityInferiorMaxPpm`) to make the AirQualitySensor's ppm
15
+ boundaries configurable instead of hardcoded
16
+
17
+ ### Changed
18
+ - ntfy notifications for an authentication failure now use a fixed,
19
+ generic message instead of the raw internal error text; other poll
20
+ failures still send the raw error message. The Homebridge log still
21
+ always shows the detailed error either way.
22
+
9
23
  ## [1.0.0] - 2026-07-26
10
24
 
11
25
  ### Changed
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Homebridge Withings Environment Data v1.0.0
1
+ # Homebridge Withings Environment Data v1.1.0
2
2
 
3
3
  **This Homebridge plugin has been 100% vibe coded with Claude.**
4
4
 
@@ -41,7 +41,8 @@ accessory:
41
41
  - **CarbonDioxideSensor**: precise CO2 level in ppm, plus a normal/abnormal
42
42
  detected alert based on a configurable threshold.
43
43
  - **AirQualitySensor**: the same CO2 reading mapped to HomeKit's
44
- Excellent/Good/Fair/Inferior/Poor category.
44
+ Excellent/Good/Fair/Inferior/Poor category, based on configurable ppm
45
+ thresholds.
45
46
  - **TemperatureSensor**: room temperature in °C.
46
47
 
47
48
  These appear automatically in the Home app after Homebridge restarts, no
@@ -68,6 +69,9 @@ Fields:
68
69
  matching the scale's own upload cadence).
69
70
  - **CO2 Detected Threshold (ppm)**: ppm above which the CarbonDioxideSensor
70
71
  reports "abnormal" (default 1000).
72
+ - **Air Quality: Excellent/Good/Fair/Inferior below (ppm)**: the four ppm
73
+ boundaries the AirQualitySensor category is based on (defaults 800, 1000,
74
+ 1500, 2000).
71
75
  - **ntfy Topic (optional)**: if set, sends a push notification via
72
76
  [ntfy.sh](https://ntfy.sh) to this topic the first time a poll fails
73
77
  (not repeated on every subsequent failure in the same streak; only once
@@ -49,6 +49,34 @@
49
49
  "default": 1000,
50
50
  "minimum": 400
51
51
  },
52
+ "airQualityExcellentMaxPpm": {
53
+ "title": "Air Quality: Excellent below (ppm)",
54
+ "type": "integer",
55
+ "default": 800,
56
+ "minimum": 1,
57
+ "description": "CO2 readings below this are reported as Excellent air quality."
58
+ },
59
+ "airQualityGoodMaxPpm": {
60
+ "title": "Air Quality: Good below (ppm)",
61
+ "type": "integer",
62
+ "default": 1000,
63
+ "minimum": 1,
64
+ "description": "CO2 readings below this are reported as Good."
65
+ },
66
+ "airQualityFairMaxPpm": {
67
+ "title": "Air Quality: Fair below (ppm)",
68
+ "type": "integer",
69
+ "default": 1500,
70
+ "minimum": 1,
71
+ "description": "CO2 readings below this are reported as Fair."
72
+ },
73
+ "airQualityInferiorMaxPpm": {
74
+ "title": "Air Quality: Inferior below (ppm)",
75
+ "type": "integer",
76
+ "default": 2000,
77
+ "minimum": 1,
78
+ "description": "CO2 readings below this are reported as Inferior. Anything above is reported as Poor."
79
+ },
52
80
  "noResponseAfterMissedPolls": {
53
81
  "title": "No Response After Missed Polls",
54
82
  "type": "integer",
package/index.js CHANGED
@@ -63,11 +63,11 @@ async function fetchLatest({ cookieHeader, sessionToken, deviceId, userId }) {
63
63
  };
64
64
  }
65
65
 
66
- function mapCo2ToAirQuality(ppm, AirQuality) {
67
- if (ppm < 800) return AirQuality.EXCELLENT;
68
- if (ppm < 1000) return AirQuality.GOOD;
69
- if (ppm < 1500) return AirQuality.FAIR;
70
- if (ppm < 2000) return AirQuality.INFERIOR;
66
+ function mapCo2ToAirQuality(ppm, AirQuality, thresholds) {
67
+ if (ppm < thresholds.excellentMaxPpm) return AirQuality.EXCELLENT;
68
+ if (ppm < thresholds.goodMaxPpm) return AirQuality.GOOD;
69
+ if (ppm < thresholds.fairMaxPpm) return AirQuality.FAIR;
70
+ if (ppm < thresholds.inferiorMaxPpm) return AirQuality.INFERIOR;
71
71
  return AirQuality.POOR;
72
72
  }
73
73
 
@@ -216,7 +216,13 @@ class WithingsEnvironmentDataPlatform {
216
216
 
217
217
  async poll() {
218
218
  try {
219
- const { cookieHeader, sessionToken } = await this.authenticate();
219
+ let cookieHeader, sessionToken;
220
+ try {
221
+ ({ cookieHeader, sessionToken } = await this.authenticate());
222
+ } catch (err) {
223
+ err.isAuthFailure = true;
224
+ throw err;
225
+ }
220
226
 
221
227
  if (!this.deviceId || !this.userId) {
222
228
  const discovered = await discoverDevice(cookieHeader);
@@ -252,7 +258,10 @@ class WithingsEnvironmentDataPlatform {
252
258
 
253
259
  if (!this.hasNotifiedFailure) {
254
260
  this.hasNotifiedFailure = true;
255
- await this.sendNtfyNotification(err.message);
261
+ const notificationMessage = err.isAuthFailure
262
+ ? 'Authentication for Withings account failed. Check the Homebridge logs for details.'
263
+ : err.message;
264
+ await this.sendNtfyNotification(notificationMessage);
256
265
  }
257
266
  }
258
267
  }
@@ -282,6 +291,19 @@ class WithingsEnvironmentDataPlatform {
282
291
  return Number.isFinite(threshold) && threshold >= 0 ? threshold : 2;
283
292
  }
284
293
 
294
+ getAirQualityThresholds() {
295
+ const excellentMaxPpm = Number(this.config.airQualityExcellentMaxPpm);
296
+ const goodMaxPpm = Number(this.config.airQualityGoodMaxPpm);
297
+ const fairMaxPpm = Number(this.config.airQualityFairMaxPpm);
298
+ const inferiorMaxPpm = Number(this.config.airQualityInferiorMaxPpm);
299
+ return {
300
+ excellentMaxPpm: Number.isFinite(excellentMaxPpm) && excellentMaxPpm > 0 ? excellentMaxPpm : 800,
301
+ goodMaxPpm: Number.isFinite(goodMaxPpm) && goodMaxPpm > 0 ? goodMaxPpm : 1000,
302
+ fairMaxPpm: Number.isFinite(fairMaxPpm) && fairMaxPpm > 0 ? fairMaxPpm : 1500,
303
+ inferiorMaxPpm: Number.isFinite(inferiorMaxPpm) && inferiorMaxPpm > 0 ? inferiorMaxPpm : 2000,
304
+ };
305
+ }
306
+
285
307
  isStale() {
286
308
  return !this.hasEverSucceeded || this.missedCycles > this.getNoResponseThreshold();
287
309
  }
@@ -306,7 +328,7 @@ class WithingsEnvironmentDataPlatform {
306
328
 
307
329
  getAirQualityOrThrow() {
308
330
  this.throwIfStale();
309
- return mapCo2ToAirQuality(this.lastReading.co2, this.Characteristic.AirQuality);
331
+ return mapCo2ToAirQuality(this.lastReading.co2, this.Characteristic.AirQuality, this.getAirQualityThresholds());
310
332
  }
311
333
 
312
334
  getTemperatureOrThrow() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-withings-environment-data",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Homebridge plugin exposing ambient CO2/air-quality and room temperature readings from a Withings WS-50 scale as HomeKit sensors",
5
5
  "main": "index.js",
6
6
  "files": [