homebridge-withings-environment-data 1.2.0 → 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 +39 -0
- package/README.md +25 -8
- package/config.schema.json +113 -38
- package/index.js +114 -38
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,45 @@ 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
|
+
|
|
32
|
+
## [1.2.1] - 2026-08-02
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- The stale-data log warning now logs on every poll while data remains
|
|
36
|
+
stale (matching how missed poll cycles are already logged), instead of
|
|
37
|
+
only once per stale streak.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- The stale-data ntfy notification no longer re-fires every time
|
|
41
|
+
Homebridge restarts while the same stale reading is still the newest one
|
|
42
|
+
on record. The "already notified about this" state, and the last-known
|
|
43
|
+
reading date it applies to, are now both persisted to disk instead of
|
|
44
|
+
living only in memory (a first fix persisted only the former, which
|
|
45
|
+
still misfired if the first poll after a restart happened to fail), and
|
|
46
|
+
still reset correctly once a fresher reading comes in.
|
|
47
|
+
|
|
9
48
|
## [1.2.0] - 2026-07-31
|
|
10
49
|
|
|
11
50
|
### Added
|
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,15 +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
|
-
nobody's stood on the scale in a while — a warning is logged
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
93
|
+
nobody's stood on the scale in a while — a warning is logged on every
|
|
94
|
+
poll for as long as it stays stale, the sensors show "No Response" in the
|
|
95
|
+
Home app, and (if ntfy Topic is set) a single notification is sent for
|
|
96
|
+
that stale reading. Default 4. This is separate from poll failures.
|
|
97
|
+
- **ntfy.sh Notification Topic** (optional): if set, sends a push notification via
|
|
81
98
|
[ntfy.sh](https://ntfy.sh) to this topic the first time a poll fails
|
|
82
99
|
(not repeated on every subsequent failure in the same streak; only once
|
|
83
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);
|
|
@@ -103,12 +105,6 @@ class WithingsEnvironmentDataPlatform {
|
|
|
103
105
|
// missed poll — reset once a poll succeeds again.
|
|
104
106
|
this.hasNotifiedFailure = false;
|
|
105
107
|
|
|
106
|
-
// Unix seconds of the newest reading actually seen so far (from the
|
|
107
|
-
// Withings measurement itself, not from a successful poll) — used to
|
|
108
|
-
// detect the scale going quiet rather than the plugin failing to poll.
|
|
109
|
-
// Warn/notify once per stale streak, reset once a fresher reading comes in.
|
|
110
|
-
this.lastReadingDate = null;
|
|
111
|
-
this.staleDataWarned = false;
|
|
112
108
|
this.warnedMissingReadingDate = false;
|
|
113
109
|
|
|
114
110
|
// The long-lived (~1 week) session_key that lets us skip email/password/2FA
|
|
@@ -117,26 +113,48 @@ class WithingsEnvironmentDataPlatform {
|
|
|
117
113
|
// it, since repeatedly hitting the password endpoint appears to be heavily
|
|
118
114
|
// throttled by Withings.
|
|
119
115
|
this.sessionStatePath = path.join(this.api.user.storagePath(), SESSION_STATE_FILENAME);
|
|
120
|
-
|
|
116
|
+
const state = this.loadPersistedState();
|
|
117
|
+
this.sessionKey = state.sessionKey ?? null;
|
|
118
|
+
// Unix seconds of the newest reading actually seen so far (from the Withings
|
|
119
|
+
// measurement itself, not from a successful poll) — used to detect the scale
|
|
120
|
+
// going quiet rather than the plugin failing to poll. Persisted and restored
|
|
121
|
+
// immediately on boot (rather than waiting on the first poll to repopulate
|
|
122
|
+
// it) so a restart can't be mistaken for "no reading yet" and wrongly clear
|
|
123
|
+
// an active stale-warning below.
|
|
124
|
+
this.lastReadingDate = state.lastReadingDate ?? null;
|
|
125
|
+
// The readingDate (unix seconds) we last sent a stale-data *notification* for,
|
|
126
|
+
// or null if not currently in a notified state. The log warning itself repeats
|
|
127
|
+
// every poll while data stays stale (like missed-poll-cycle failures do), but
|
|
128
|
+
// this gates the ntfy notification to once per distinct stale reading, and is
|
|
129
|
+
// persisted so a Homebridge restart doesn't re-notify about a reading we've
|
|
130
|
+
// already notified about — it only resets once a fresher reading comes in.
|
|
131
|
+
this.staleNotifiedForReadingDate = state.staleNotifiedForReadingDate ?? null;
|
|
121
132
|
|
|
122
133
|
this.api.on('didFinishLaunching', () => this.discoverDevices());
|
|
123
134
|
}
|
|
124
135
|
|
|
125
|
-
|
|
136
|
+
loadPersistedState() {
|
|
126
137
|
try {
|
|
127
138
|
const raw = fs.readFileSync(this.sessionStatePath, 'utf8');
|
|
128
|
-
return JSON.parse(raw)
|
|
139
|
+
return JSON.parse(raw);
|
|
129
140
|
} catch (err) {
|
|
130
141
|
if (err.code !== 'ENOENT') {
|
|
131
142
|
this.log.warn(`Could not read session state file: ${err.message}`);
|
|
132
143
|
}
|
|
133
|
-
return
|
|
144
|
+
return {};
|
|
134
145
|
}
|
|
135
146
|
}
|
|
136
147
|
|
|
137
|
-
|
|
148
|
+
persistState() {
|
|
138
149
|
try {
|
|
139
|
-
fs.writeFileSync(
|
|
150
|
+
fs.writeFileSync(
|
|
151
|
+
this.sessionStatePath,
|
|
152
|
+
JSON.stringify({
|
|
153
|
+
sessionKey: this.sessionKey,
|
|
154
|
+
lastReadingDate: this.lastReadingDate,
|
|
155
|
+
staleNotifiedForReadingDate: this.staleNotifiedForReadingDate,
|
|
156
|
+
})
|
|
157
|
+
);
|
|
140
158
|
} catch (err) {
|
|
141
159
|
this.log.warn(`Could not persist session state file: ${err.message}`);
|
|
142
160
|
}
|
|
@@ -161,7 +179,7 @@ class WithingsEnvironmentDataPlatform {
|
|
|
161
179
|
if (result.sessionKey) {
|
|
162
180
|
if (result.sessionKey !== this.sessionKey) {
|
|
163
181
|
this.sessionKey = result.sessionKey;
|
|
164
|
-
this.
|
|
182
|
+
this.persistState();
|
|
165
183
|
this.log.info('Withings full login succeeded; cached the new session for future polls.');
|
|
166
184
|
} else {
|
|
167
185
|
this.log.info('Withings full login succeeded; session token unchanged.');
|
|
@@ -182,15 +200,54 @@ class WithingsEnvironmentDataPlatform {
|
|
|
182
200
|
const uuid = this.api.hap.uuid.generate('withings-environment-data');
|
|
183
201
|
let accessory = this.accessories.find((acc) => acc.UUID === uuid);
|
|
184
202
|
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
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]);
|
|
188
213
|
}
|
|
189
214
|
|
|
190
|
-
this.
|
|
215
|
+
this.connectMqtt();
|
|
191
216
|
this.startPolling();
|
|
192
217
|
}
|
|
193
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
|
+
|
|
194
251
|
setupServices(accessory) {
|
|
195
252
|
this.infoService = accessory.getService(this.Service.AccessoryInformation);
|
|
196
253
|
if (this.infoService) {
|
|
@@ -354,16 +411,22 @@ class WithingsEnvironmentDataPlatform {
|
|
|
354
411
|
async checkStaleData() {
|
|
355
412
|
const stale = this.isDataStale();
|
|
356
413
|
|
|
357
|
-
if (stale
|
|
358
|
-
this.staleDataWarned = true;
|
|
414
|
+
if (stale) {
|
|
359
415
|
const { date, time } = this.formatStaleDateTime(this.lastReadingDate);
|
|
360
416
|
this.log.warn(`Withings data is stale: last recorded at ${date} ${time}`);
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
417
|
+
|
|
418
|
+
const alreadyNotifiedForThisReading = this.staleNotifiedForReadingDate === this.lastReadingDate;
|
|
419
|
+
if (!alreadyNotifiedForThisReading) {
|
|
420
|
+
this.staleNotifiedForReadingDate = this.lastReadingDate;
|
|
421
|
+
this.persistState();
|
|
422
|
+
await this.sendNtfyNotification(
|
|
423
|
+
`Temperature and/or CO2 readings haven't been updated since ${date} at ${time}.`,
|
|
424
|
+
'Homebridge: Withings Environment Data is out of date'
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
} else if (this.staleNotifiedForReadingDate !== null) {
|
|
428
|
+
this.staleNotifiedForReadingDate = null;
|
|
429
|
+
this.persistState();
|
|
367
430
|
this.log.info('Withings: fresh data has been recorded.');
|
|
368
431
|
}
|
|
369
432
|
}
|
|
@@ -411,34 +474,47 @@ class WithingsEnvironmentDataPlatform {
|
|
|
411
474
|
'Withings measurement data has no recognizable date field; stale data detection is disabled until this is fixed.'
|
|
412
475
|
);
|
|
413
476
|
}
|
|
414
|
-
} else if (readingDate !== null && readingDate !== undefined) {
|
|
477
|
+
} else if (readingDate !== null && readingDate !== undefined && readingDate !== this.lastReadingDate) {
|
|
415
478
|
this.lastReadingDate = readingDate;
|
|
479
|
+
this.persistState();
|
|
416
480
|
}
|
|
417
481
|
|
|
418
482
|
if (co2 !== null && co2 !== undefined) {
|
|
419
483
|
this.lastReading.co2 = co2;
|
|
420
484
|
this.hasEverSucceeded = true;
|
|
421
|
-
this.co2Service
|
|
422
|
-
|
|
423
|
-
this.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
+
}
|
|
432
500
|
}
|
|
433
501
|
|
|
434
502
|
if (temperature !== null && temperature !== undefined) {
|
|
435
503
|
this.lastReading.temperature = temperature;
|
|
436
504
|
this.hasEverSucceeded = true;
|
|
437
|
-
this.temperatureService
|
|
505
|
+
if (this.temperatureService) {
|
|
506
|
+
this.temperatureService.updateCharacteristic(this.Characteristic.CurrentTemperature, temperature);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (hasReading) {
|
|
511
|
+
this.publishMqttReading();
|
|
438
512
|
}
|
|
439
513
|
}
|
|
440
514
|
|
|
441
515
|
setFault(hasFault) {
|
|
516
|
+
if (!this.co2Service) return;
|
|
517
|
+
|
|
442
518
|
const value = hasFault
|
|
443
519
|
? this.Characteristic.StatusFault.GENERAL_FAULT
|
|
444
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"
|