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 +23 -0
- package/README.md +21 -5
- package/config.schema.json +113 -38
- package/index.js +69 -16
- package/package.json +5 -1
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
|
-
#
|
|
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
|
-
- **
|
|
73
|
-
|
|
74
|
-
|
|
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)
|
|
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
|
package/config.schema.json
CHANGED
|
@@ -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
|
-
"
|
|
41
|
-
"title": "
|
|
42
|
-
"type": "
|
|
43
|
-
"default":
|
|
44
|
-
"minimum": 5
|
|
77
|
+
"exposeAsHomekitAccessories": {
|
|
78
|
+
"title": "Expose sensors as HomeKit Accessories",
|
|
79
|
+
"type": "boolean",
|
|
80
|
+
"default": true
|
|
45
81
|
},
|
|
46
|
-
"
|
|
47
|
-
"title": "
|
|
48
|
-
"type": "
|
|
49
|
-
"default":
|
|
50
|
-
"
|
|
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
|
-
"
|
|
53
|
-
"title": "
|
|
54
|
-
"type": "
|
|
55
|
-
"
|
|
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
|
-
"
|
|
60
|
-
"title": "
|
|
93
|
+
"mqttPort": {
|
|
94
|
+
"title": "Port",
|
|
61
95
|
"type": "integer",
|
|
62
|
-
"default":
|
|
63
|
-
"minimum": 1,
|
|
64
|
-
"description": "CO2 readings below this are reported as Good."
|
|
96
|
+
"default": 1883
|
|
65
97
|
},
|
|
66
|
-
"
|
|
67
|
-
"title": "
|
|
68
|
-
"type": "
|
|
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
|
-
"
|
|
74
|
-
"title": "
|
|
75
|
-
"type": "
|
|
76
|
-
"
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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
|
|
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
|
|
124
|
+
"title": "ntfy.sh Notification Topic",
|
|
96
125
|
"type": "string",
|
|
97
|
-
"description": "If set, sends a push notification via ntfy.sh
|
|
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 (
|
|
202
|
-
|
|
203
|
-
|
|
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.
|
|
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
|
|
445
|
-
|
|
446
|
-
this.
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
|
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
|
-
"
|
|
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"
|