homebridge-withings-environment-data 1.2.1 → 1.3.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,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## [1.3.0] - 2026-08-03
10
+
11
+ ### Added
12
+ - Optional MQTT publishing: when "Publish to MQTT" is enabled and a broker
13
+ Host is configured, every successful poll publishes a retained message
14
+ to topic `withingsenv/ws-50` shaped like
15
+ `{"temperature": 25.2, "co2_levels": 674}`
16
+ - "Expose sensors as HomeKit Accessories" toggle (default on): when off,
17
+ the plugin still polls (and still publishes to MQTT, if enabled) but
18
+ doesn't create or update a HomeKit accessory
19
+
20
+ ### Changed
21
+ - Config UI reorganized: Name, then an expanded "Authentication" fieldset,
22
+ the Expose/Publish checkboxes, a collapsed "MQTT" fieldset, a collapsed
23
+ "Air Quality" fieldset (shorter field titles, fewer descriptions), Poll
24
+ Interval, No Response After Missed Polls, Stale Data Warning Threshold,
25
+ ntfy.sh Notification Topic (renamed from "ntfy Topic (optional)")
26
+ - `displayName` set in `package.json` so Homebridge Config UI shows
27
+ "Withings Environment Data" in the Plugins list instead of the raw npm
28
+ package name
29
+ - Shortened a few config field descriptions (No Response After Missed
30
+ Polls, Stale Data Warning Threshold, ntfy.sh Notification Topic)
31
+
9
32
  ## [1.2.1] - 2026-08-02
10
33
 
11
34
  ### Changed
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Homebridge Withings Environment Data v1.2.1
1
+ # Withings Environment Data v1.3.0
2
2
 
3
3
  **This Homebridge plugin has been 100% vibe coded with Claude.**
4
4
 
@@ -48,6 +48,17 @@ accessory:
48
48
  These appear automatically in the Home app after Homebridge restarts, no
49
49
  further setup needed. Readings update on the poll interval set below.
50
50
 
51
+ HomeKit exposure and MQTT publishing are independent and can each be
52
+ turned on or off in Configuration:
53
+
54
+ - **Expose sensors as HomeKit Accessories** (default on): when off, the
55
+ plugin still polls Withings (and still publishes to MQTT, if enabled),
56
+ but doesn't create or update any HomeKit accessory.
57
+ - **Publish to MQTT** (default off): when on, every successful poll
58
+ publishes a retained message to topic `withingsenv/ws-50` on the
59
+ configured broker, shaped like
60
+ `{"temperature": 25.2, "co2_levels": 674}`.
61
+
51
62
  ### Configuration
52
63
 
53
64
  All settings are entered through the Homebridge Config UI (Plugins tab,
@@ -69,16 +80,21 @@ Fields:
69
80
  matching the scale's own upload cadence).
70
81
  - **CO2 Detected Threshold (ppm)**: ppm above which the CarbonDioxideSensor
71
82
  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).
83
+ - **Excellent/Good/Fair/Inferior below (ppm)**: the four ppm boundaries the
84
+ AirQualitySensor category is based on (defaults 800, 1000, 1500, 2000).
85
+ Anything above the Inferior boundary is reported as Poor.
86
+ - **Expose sensors as HomeKit Accessories**: see [Usage](#usage) above.
87
+ Default on.
88
+ - **Publish to MQTT / Host / Port / Username / Password**: see
89
+ [Usage](#usage) above. Publishing is off by default; Port defaults to
90
+ 1883. Username/Password are optional, for brokers that require auth.
75
91
  - **Stale Data Warning Threshold (hours)**: if the newest reading from the
76
92
  scale itself (not the plugin's poll) is older than this many hours — e.g.
77
93
  nobody's stood on the scale in a while — a warning is logged on every
78
94
  poll for as long as it stays stale, the sensors show "No Response" in the
79
95
  Home app, and (if ntfy Topic is set) a single notification is sent for
80
96
  that stale reading. Default 4. This is separate from poll failures.
81
- - **ntfy Topic (optional)**: if set, sends a push notification via
97
+ - **ntfy.sh Notification Topic** (optional): if set, sends a push notification via
82
98
  [ntfy.sh](https://ntfy.sh) to this topic the first time a poll fails
83
99
  (not repeated on every subsequent failure in the same streak; only once
84
100
  a poll succeeds again does the next failure trigger a fresh
@@ -12,6 +12,43 @@
12
12
  "default": "Withings Environment",
13
13
  "required": true
14
14
  },
15
+ "co2DetectedThresholdPpm": {
16
+ "title": "CO2 Detected Threshold (ppm)",
17
+ "type": "integer",
18
+ "default": 1000,
19
+ "minimum": 400
20
+ },
21
+ "airQualityExcellentMaxPpm": {
22
+ "title": "Excellent below (ppm)",
23
+ "type": "integer",
24
+ "default": 800,
25
+ "minimum": 1
26
+ },
27
+ "airQualityGoodMaxPpm": {
28
+ "title": "Good below (ppm)",
29
+ "type": "integer",
30
+ "default": 1000,
31
+ "minimum": 1
32
+ },
33
+ "airQualityFairMaxPpm": {
34
+ "title": "Fair below (ppm)",
35
+ "type": "integer",
36
+ "default": 1500,
37
+ "minimum": 1
38
+ },
39
+ "airQualityInferiorMaxPpm": {
40
+ "title": "Inferior below (ppm)",
41
+ "type": "integer",
42
+ "default": 2000,
43
+ "minimum": 1,
44
+ "description": "Anything above is reported as Poor."
45
+ },
46
+ "pollIntervalMinutes": {
47
+ "title": "Poll Interval (minutes)",
48
+ "type": "integer",
49
+ "default": 30,
50
+ "minimum": 5
51
+ },
15
52
  "email": {
16
53
  "title": "Withings Email",
17
54
  "type": "string",
@@ -37,65 +74,103 @@
37
74
  "required": true,
38
75
  "description": "The value of that same 2fa_token_<hash> cookie"
39
76
  },
40
- "pollIntervalMinutes": {
41
- "title": "Poll Interval (minutes)",
42
- "type": "integer",
43
- "default": 30,
44
- "minimum": 5
77
+ "exposeAsHomekitAccessories": {
78
+ "title": "Expose sensors as HomeKit Accessories",
79
+ "type": "boolean",
80
+ "default": true
45
81
  },
46
- "co2DetectedThresholdPpm": {
47
- "title": "CO2 Detected Threshold (ppm)",
48
- "type": "integer",
49
- "default": 1000,
50
- "minimum": 400
82
+ "publishToMqtt": {
83
+ "title": "Publish to MQTT",
84
+ "type": "boolean",
85
+ "default": false,
86
+ "description": "Publish sensor data to configured MQTT broker. Topic name: \"withingsenv/ws-50\""
51
87
  },
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."
88
+ "mqttHost": {
89
+ "title": "Host",
90
+ "type": "string",
91
+ "description": "Broker hostname or IP address."
58
92
  },
59
- "airQualityGoodMaxPpm": {
60
- "title": "Air Quality: Good below (ppm)",
93
+ "mqttPort": {
94
+ "title": "Port",
61
95
  "type": "integer",
62
- "default": 1000,
63
- "minimum": 1,
64
- "description": "CO2 readings below this are reported as Good."
96
+ "default": 1883
65
97
  },
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."
98
+ "mqttUsername": {
99
+ "title": "Username",
100
+ "type": "string"
72
101
  },
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."
102
+ "mqttPassword": {
103
+ "title": "Password",
104
+ "type": "string",
105
+ "x-schema-form": {
106
+ "type": "password"
107
+ }
79
108
  },
80
109
  "noResponseAfterMissedPolls": {
81
110
  "title": "No Response After Missed Polls",
82
111
  "type": "integer",
83
112
  "default": 2,
84
113
  "minimum": 0,
85
- "description": "Consecutive failed polls to tolerate (keeping the last known readings) before the Home app shows \"No Response\". 0 means show it immediately on any failure."
114
+ "description": "Consecutive failed polls to tolerate before the Home app shows \"No Response\"."
86
115
  },
87
116
  "staleDataWarningThresholdHours": {
88
117
  "title": "Stale Data Warning Threshold (hours)",
89
118
  "type": "integer",
90
119
  "default": 4,
91
120
  "minimum": 1,
92
- "description": "If the newest CO2/temperature reading from the scale itself is older than this many hours (e.g. nobody's stood on it), log a warning, show \"No Response\" in the Home app, and (if ntfy Topic is set) send a notification. This is separate from poll failures above."
121
+ "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."
93
122
  },
94
123
  "ntfyTopic": {
95
- "title": "ntfy Topic (optional)",
124
+ "title": "ntfy.sh Notification Topic",
96
125
  "type": "string",
97
- "description": "If set, sends a push notification via ntfy.sh to this topic the first time a poll fails (not repeated on every subsequent failure in the same streak). Leave blank to disable."
126
+ "description": "If set, sends a push notification via ntfy.sh the first time a poll fails."
98
127
  }
99
128
  }
100
- }
129
+ },
130
+ "layout": [
131
+ "name",
132
+ {
133
+ "type": "fieldset",
134
+ "title": "Authentication",
135
+ "expandable": true,
136
+ "expanded": true,
137
+ "items": [
138
+ "email",
139
+ "password",
140
+ "trustCookieName",
141
+ "trustCookieValue"
142
+ ]
143
+ },
144
+ "exposeAsHomekitAccessories",
145
+ "publishToMqtt",
146
+ {
147
+ "type": "fieldset",
148
+ "title": "MQTT",
149
+ "expandable": true,
150
+ "expanded": false,
151
+ "items": [
152
+ "mqttHost",
153
+ "mqttPort",
154
+ "mqttUsername",
155
+ "mqttPassword"
156
+ ]
157
+ },
158
+ {
159
+ "type": "fieldset",
160
+ "title": "Air Quality",
161
+ "expandable": true,
162
+ "expanded": false,
163
+ "items": [
164
+ "co2DetectedThresholdPpm",
165
+ "airQualityExcellentMaxPpm",
166
+ "airQualityGoodMaxPpm",
167
+ "airQualityFairMaxPpm",
168
+ "airQualityInferiorMaxPpm"
169
+ ]
170
+ },
171
+ "pollIntervalMinutes",
172
+ "noResponseAfterMissedPolls",
173
+ "staleDataWarningThresholdHours",
174
+ "ntfyTopic"
175
+ ]
101
176
  }
package/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const mqtt = require('mqtt');
3
4
  const { login, resumeSession } = require('./lib/login');
4
5
  const { discoverDevice } = require('./lib/discover');
5
6
 
@@ -16,6 +17,7 @@ const APPPFM = 'web';
16
17
  const WINDOW_HOURS = 48;
17
18
 
18
19
  const PLATFORM_NAME = 'WithingsEnvironmentData';
20
+ const MQTT_TOPIC = 'withingsenv/ws-50';
19
21
 
20
22
  async function fetchLatest({ cookieHeader, sessionToken, deviceId, userId }) {
21
23
  const enddate = Math.floor(Date.now() / 1000);
@@ -198,15 +200,54 @@ class WithingsEnvironmentDataPlatform {
198
200
  const uuid = this.api.hap.uuid.generate('withings-environment-data');
199
201
  let accessory = this.accessories.find((acc) => acc.UUID === uuid);
200
202
 
201
- if (!accessory) {
202
- accessory = new this.api.platformAccessory(this.config.name || 'Withings Environment', uuid);
203
- this.api.registerPlatformAccessories('homebridge-withings-environment-data', PLATFORM_NAME, [accessory]);
203
+ if (this.getExposeAsHomekitAccessories()) {
204
+ if (!accessory) {
205
+ accessory = new this.api.platformAccessory(this.config.name || 'Withings Environment', uuid);
206
+ this.api.registerPlatformAccessories('homebridge-withings-environment-data', PLATFORM_NAME, [accessory]);
207
+ }
208
+ this.setupServices(accessory);
209
+ } else if (accessory) {
210
+ // Previously exposed, now toggled off — remove the stale accessory rather
211
+ // than leaving it registered but never updated.
212
+ this.api.unregisterPlatformAccessories('homebridge-withings-environment-data', PLATFORM_NAME, [accessory]);
204
213
  }
205
214
 
206
- this.setupServices(accessory);
215
+ this.connectMqtt();
207
216
  this.startPolling();
208
217
  }
209
218
 
219
+ getExposeAsHomekitAccessories() {
220
+ return this.config.exposeAsHomekitAccessories !== false;
221
+ }
222
+
223
+ getPublishToMqtt() {
224
+ return this.config.publishToMqtt === true;
225
+ }
226
+
227
+ connectMqtt() {
228
+ if (!this.getPublishToMqtt()) return;
229
+
230
+ if (!this.config.mqttHost) {
231
+ this.log.warn('Publish to MQTT is enabled but no MQTT Host is configured; MQTT publishing is disabled.');
232
+ return;
233
+ }
234
+
235
+ const port = Number(this.config.mqttPort);
236
+ const url = `mqtt://${this.config.mqttHost}:${Number.isFinite(port) && port > 0 ? port : 1883}`;
237
+ this.mqttClient = mqtt.connect(url, {
238
+ username: this.config.mqttUsername || undefined,
239
+ password: this.config.mqttPassword || undefined,
240
+ });
241
+ this.mqttClient.on('error', (err) => this.log.warn(`MQTT connection error: ${err.message}`));
242
+ this.api.on('shutdown', () => this.mqttClient.end());
243
+ }
244
+
245
+ publishMqttReading() {
246
+ if (!this.mqttClient) return;
247
+ const payload = JSON.stringify({ temperature: this.lastReading.temperature, co2_levels: this.lastReading.co2 });
248
+ this.mqttClient.publish(MQTT_TOPIC, payload, { retain: true });
249
+ }
250
+
210
251
  setupServices(accessory) {
211
252
  this.infoService = accessory.getService(this.Service.AccessoryInformation);
212
253
  if (this.infoService) {
@@ -441,27 +482,39 @@ class WithingsEnvironmentDataPlatform {
441
482
  if (co2 !== null && co2 !== undefined) {
442
483
  this.lastReading.co2 = co2;
443
484
  this.hasEverSucceeded = true;
444
- this.co2Service.updateCharacteristic(this.Characteristic.CarbonDioxideLevel, co2);
445
- this.co2Service.updateCharacteristic(
446
- this.Characteristic.CarbonDioxideDetected,
447
- co2 > co2Threshold
448
- ? this.Characteristic.CarbonDioxideDetected.CO2_LEVELS_ABNORMAL
449
- : this.Characteristic.CarbonDioxideDetected.CO2_LEVELS_NORMAL
450
- );
451
- this.airQualityService.updateCharacteristic(
452
- this.Characteristic.AirQuality,
453
- mapCo2ToAirQuality(co2, this.Characteristic.AirQuality, this.getAirQualityThresholds())
454
- );
485
+ if (this.co2Service) {
486
+ this.co2Service.updateCharacteristic(this.Characteristic.CarbonDioxideLevel, co2);
487
+ this.co2Service.updateCharacteristic(
488
+ this.Characteristic.CarbonDioxideDetected,
489
+ co2 > co2Threshold
490
+ ? this.Characteristic.CarbonDioxideDetected.CO2_LEVELS_ABNORMAL
491
+ : this.Characteristic.CarbonDioxideDetected.CO2_LEVELS_NORMAL
492
+ );
493
+ }
494
+ if (this.airQualityService) {
495
+ this.airQualityService.updateCharacteristic(
496
+ this.Characteristic.AirQuality,
497
+ mapCo2ToAirQuality(co2, this.Characteristic.AirQuality, this.getAirQualityThresholds())
498
+ );
499
+ }
455
500
  }
456
501
 
457
502
  if (temperature !== null && temperature !== undefined) {
458
503
  this.lastReading.temperature = temperature;
459
504
  this.hasEverSucceeded = true;
460
- this.temperatureService.updateCharacteristic(this.Characteristic.CurrentTemperature, temperature);
505
+ if (this.temperatureService) {
506
+ this.temperatureService.updateCharacteristic(this.Characteristic.CurrentTemperature, temperature);
507
+ }
508
+ }
509
+
510
+ if (hasReading) {
511
+ this.publishMqttReading();
461
512
  }
462
513
  }
463
514
 
464
515
  setFault(hasFault) {
516
+ if (!this.co2Service) return;
517
+
465
518
  const value = hasFault
466
519
  ? this.Characteristic.StatusFault.GENERAL_FAULT
467
520
  : this.Characteristic.StatusFault.NO_FAULT;
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-withings-environment-data",
3
- "version": "1.2.1",
3
+ "displayName": "Withings Environment Data",
4
+ "version": "1.3.0",
4
5
  "description": "Homebridge plugin exposing ambient CO2/air-quality and room temperature readings from a Withings WS-50 scale as HomeKit sensors",
5
6
  "main": "index.js",
6
7
  "files": [
@@ -23,6 +24,9 @@
23
24
  ],
24
25
  "author": "Luud Jacobs",
25
26
  "license": "MIT",
27
+ "dependencies": {
28
+ "mqtt": "^5.15.0"
29
+ },
26
30
  "homepage": "https://github.com/LuudJacobs/homebridge-withings-environment-data#readme",
27
31
  "bugs": {
28
32
  "url": "https://github.com/LuudJacobs/homebridge-withings-environment-data/issues"