homebridge-withings-environment-data 0.1.3

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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(gh auth *)",
5
+ "Bash(gh repo *)"
6
+ ]
7
+ }
8
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "workspace.name" : "Withings Enironment Data"
3
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Luuuud
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # Homebridge Withings Environment Data
2
+
3
+ **This Homebridge plugin has been 100% vibe coded with Claude.**
4
+
5
+ Exposes ambient CO2 (air quality) and room temperature readings from a
6
+ Withings WS-50 scale as HomeKit sensors:
7
+
8
+ - **CarbonDioxideSensor**: precise CO2 level in ppm, plus a normal/abnormal
9
+ detected alert based on a configurable threshold.
10
+ - **AirQualitySensor**: the same CO2 reading mapped to HomeKit's
11
+ Excellent/Good/Fair/Inferior/Poor category.
12
+ - **TemperatureSensor**: room temperature in °C.
13
+
14
+ This data isn't available through the official Withings API: a `getmeas`
15
+ call against the documented endpoint drops CO2/temperature even when
16
+ explicitly requested. Instead this plugin authenticates the same way the
17
+ Health Mate web app does (session cookies, not OAuth) and calls the same
18
+ internal endpoint the web app uses. That means it depends on undocumented
19
+ behavior of `account.withings.com` / `scalews.withings.com` and could break
20
+ if Withings changes them.
21
+
22
+ ## Installation
23
+
24
+ Install via the Homebridge Config UI: open the **Plugins** tab, search for
25
+ "Withings Environment Data", and click **Install**.
26
+
27
+ Or from the command line:
28
+
29
+ ```bash
30
+ npm install -g homebridge-withings-environment-data
31
+ ```
32
+
33
+ Then restart Homebridge and add the platform via the Config UI, or add it
34
+ manually to `config.json`.
35
+
36
+ ## Configuration
37
+
38
+ All settings are entered through the Homebridge Config UI (Plugins tab,
39
+ Withings Environment Data, Settings).
40
+
41
+ > **Note:** Homebridge stores your password and trust cookie value in plain
42
+ > text in its `config.json` file on disk. Anyone with access to that file,
43
+ > or to the Homebridge host itself, can read them. Treat that file with the
44
+ > same care as any other credentials store.
45
+
46
+ Fields:
47
+
48
+ - **Withings Email / Password**: your account credentials. Used only
49
+ against `account.withings.com`'s own login form.
50
+ - **Trust Cookie Name / Value**: see [Getting the trust
51
+ cookie](#getting-the-trust-cookie) below. As long as this is reused, the
52
+ plugin's automated logins skip the 2FA prompt.
53
+ - **Poll Interval (minutes)**: how often to fetch new readings (default 30,
54
+ matching the scale's own upload cadence).
55
+ - **CO2 Detected Threshold (ppm)**: ppm above which the CarbonDioxideSensor
56
+ reports "abnormal" (default 1000).
57
+
58
+ ## When it stops working
59
+
60
+ If the Homebridge log shows a "session not trusted (landed on confirm_totp)"
61
+ error, the device-trust cookie has been invalidated (e.g. after a password
62
+ change, or Withings revoking trusted devices). The Home app will keep
63
+ showing the last known good reading, it won't go blank, but a fault
64
+ indicator appears on the sensors until this is fixed. Fix:
65
+
66
+ 1. Recapture the trust cookie: see [Getting the trust
67
+ cookie](#getting-the-trust-cookie) below.
68
+ 2. Update the **Trust Cookie Value** field in the plugin's Config UI
69
+ settings (the **Trust Cookie Name** itself should stay the same).
70
+
71
+ Any other poll failure (network error, unexpected response) is logged the
72
+ same way: an error in the Homebridge log, a fault indicator on the sensors,
73
+ and the previous readings left in place until the next successful poll.
74
+
75
+ ## Getting the trust cookie
76
+
77
+ Both initial setup and recovering from an expired session need this. Capture
78
+ it once via DevTools:
79
+
80
+ 1. Log into [account.withings.com](https://account.withings.com) in a
81
+ browser, making sure to check **"trust this device"** when prompted for
82
+ a 2FA code.
83
+ 2. Open DevTools, Application tab, Cookies, `account.withings.com`.
84
+ 3. Find the cookie named `2fa_token_<a long hex hash>`, then copy its full
85
+ name and its value. This is what actually signals "this device already
86
+ passed 2FA". Its name stays stable across logins even though its value
87
+ doesn't.
@@ -0,0 +1,54 @@
1
+ {
2
+ "pluginAlias": "WithingsEnvironmentData",
3
+ "pluginType": "platform",
4
+ "singular": true,
5
+ "headerDisplay": "Exposes ambient CO2/air-quality and room temperature from a Withings WS-50 scale as HomeKit sensors. This data isn't available through the official Withings API, so this plugin authenticates the same way the Health Mate web app does. See the README for how to capture the config values below via browser DevTools.",
6
+ "schema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "name": {
10
+ "title": "Name",
11
+ "type": "string",
12
+ "default": "Withings Environment",
13
+ "required": true
14
+ },
15
+ "email": {
16
+ "title": "Withings Email",
17
+ "type": "string",
18
+ "required": true
19
+ },
20
+ "password": {
21
+ "title": "Withings Password",
22
+ "type": "string",
23
+ "required": true,
24
+ "x-schema-form": {
25
+ "type": "password"
26
+ }
27
+ },
28
+ "trustCookieName": {
29
+ "title": "Trust Cookie Name",
30
+ "type": "string",
31
+ "required": true,
32
+ "description": "The cookie named 2fa_token_<long hex hash> under account.withings.com — copy its full name here"
33
+ },
34
+ "trustCookieValue": {
35
+ "title": "Trust Cookie Value",
36
+ "type": "string",
37
+ "required": true,
38
+ "description": "The value of that same 2fa_token_<hash> cookie"
39
+ },
40
+ "pollIntervalMinutes": {
41
+ "title": "Poll Interval (minutes)",
42
+ "type": "integer",
43
+ "default": 30,
44
+ "minimum": 5
45
+ },
46
+ "co2DetectedThresholdPpm": {
47
+ "title": "CO2 Detected Threshold (ppm)",
48
+ "type": "integer",
49
+ "default": 1000,
50
+ "minimum": 400
51
+ }
52
+ }
53
+ }
54
+ }
package/index.js ADDED
@@ -0,0 +1,208 @@
1
+ const { login } = require('./lib/login');
2
+ const { discoverDevice } = require('./lib/discover');
3
+
4
+ const MEASURE_URL = 'https://scalews.withings.com/cgi-bin/v2/measure';
5
+ // Reverse-engineered/unofficial: not part of the documented Withings API.
6
+ const MEASTYPE_CO2 = 35;
7
+ const MEASTYPE_TEMPERATURE = 12;
8
+ const APPNAME = 'hmw';
9
+ const APPPFM = 'web';
10
+ // Wide enough to reliably find at least one recent point even after a longer
11
+ // gap in scale uploads — only the single newest point per series is used.
12
+ const WINDOW_HOURS = 48;
13
+
14
+ const PLATFORM_NAME = 'WithingsEnvironmentData';
15
+
16
+ async function fetchLatest({ cookieHeader, sessionToken, deviceId, userId }) {
17
+ const enddate = Math.floor(Date.now() / 1000);
18
+ const startdate = enddate - WINDOW_HOURS * 3600;
19
+
20
+ const body = new URLSearchParams({
21
+ action: 'getmeashf',
22
+ deviceid: deviceId,
23
+ userid: userId,
24
+ meastype: `${MEASTYPE_CO2},${MEASTYPE_TEMPERATURE}`,
25
+ startdate: String(startdate),
26
+ enddate: String(enddate),
27
+ appname: APPNAME,
28
+ apppfm: APPPFM,
29
+ session_token: sessionToken,
30
+ }).toString();
31
+
32
+ const response = await fetch(MEASURE_URL, {
33
+ method: 'POST',
34
+ headers: {
35
+ 'Content-Type': 'text/plain;charset=UTF-8',
36
+ Origin: 'https://app.withings.com',
37
+ Referer: 'https://app.withings.com/',
38
+ Cookie: cookieHeader,
39
+ },
40
+ body,
41
+ });
42
+
43
+ if (!response.ok) {
44
+ throw new Error(`Measure request failed with HTTP status ${response.status}`);
45
+ }
46
+
47
+ const json = await response.json();
48
+ if (json.status !== 0) {
49
+ throw new Error(`Measure request returned Withings status ${json.status}: ${JSON.stringify(json.error ?? json)}`);
50
+ }
51
+
52
+ const co2Series = json.body.series?.find((s) => s.type === MEASTYPE_CO2)?.data ?? [];
53
+ const tempSeries = json.body.series?.find((s) => s.type === MEASTYPE_TEMPERATURE)?.data ?? [];
54
+
55
+ return {
56
+ // Both series are newest-first, so the first entry is the latest reading.
57
+ co2: co2Series.length > 0 ? co2Series[0].value : null,
58
+ temperature: tempSeries.length > 0 ? tempSeries[0].value : null,
59
+ };
60
+ }
61
+
62
+ function mapCo2ToAirQuality(ppm, AirQuality) {
63
+ if (ppm < 800) return AirQuality.EXCELLENT;
64
+ if (ppm < 1000) return AirQuality.GOOD;
65
+ if (ppm < 1500) return AirQuality.FAIR;
66
+ if (ppm < 2000) return AirQuality.INFERIOR;
67
+ return AirQuality.POOR;
68
+ }
69
+
70
+ class WithingsEnvironmentDataPlatform {
71
+ constructor(log, config, api) {
72
+ this.log = log;
73
+ this.config = config;
74
+ this.api = api;
75
+ this.Service = api.hap.Service;
76
+ this.Characteristic = api.hap.Characteristic;
77
+ this.accessories = [];
78
+ // Discovered automatically from the Withings association endpoint on first
79
+ // successful login, rather than requiring the user to hunt for these in
80
+ // DevTools — see lib/discover.js.
81
+ this.deviceId = null;
82
+ this.userId = null;
83
+
84
+ this.api.on('didFinishLaunching', () => this.discoverDevices());
85
+ }
86
+
87
+ configureAccessory(accessory) {
88
+ this.log.info('Loading accessory from cache:', accessory.displayName);
89
+ this.accessories.push(accessory);
90
+ }
91
+
92
+ discoverDevices() {
93
+ const uuid = this.api.hap.uuid.generate('withings-environment-data');
94
+ let accessory = this.accessories.find((acc) => acc.UUID === uuid);
95
+
96
+ if (!accessory) {
97
+ accessory = new this.api.platformAccessory(this.config.name || 'Withings Environment', uuid);
98
+ this.api.registerPlatformAccessories('homebridge-withings-environment-data', PLATFORM_NAME, [accessory]);
99
+ }
100
+
101
+ this.setupServices(accessory);
102
+ this.startPolling();
103
+ }
104
+
105
+ setupServices(accessory) {
106
+ this.infoService = accessory.getService(this.Service.AccessoryInformation);
107
+ if (this.infoService) {
108
+ this.infoService
109
+ .setCharacteristic(this.Characteristic.Manufacturer, 'Withings')
110
+ .setCharacteristic(this.Characteristic.Model, 'WS-50')
111
+ .setCharacteristic(this.Characteristic.SerialNumber, 'pending discovery');
112
+ }
113
+
114
+ this.co2Service =
115
+ accessory.getService(this.Service.CarbonDioxideSensor) ||
116
+ accessory.addService(this.Service.CarbonDioxideSensor, 'CO2', 'co2');
117
+
118
+ this.airQualityService =
119
+ accessory.getService(this.Service.AirQualitySensor) ||
120
+ accessory.addService(this.Service.AirQualitySensor, 'Air Quality', 'air-quality');
121
+
122
+ this.temperatureService =
123
+ accessory.getService(this.Service.TemperatureSensor) ||
124
+ accessory.addService(this.Service.TemperatureSensor, 'Temperature', 'temperature');
125
+ }
126
+
127
+ startPolling() {
128
+ const minutes = Number(this.config.pollIntervalMinutes);
129
+ const intervalMs = (Number.isFinite(minutes) && minutes > 0 ? minutes : 30) * 60 * 1000;
130
+
131
+ this.poll();
132
+ this.pollTimer = setInterval(() => this.poll(), intervalMs);
133
+ }
134
+
135
+ async poll() {
136
+ try {
137
+ const { cookieHeader, sessionToken } = await login(
138
+ this.config.email,
139
+ this.config.password,
140
+ this.config.trustCookieName,
141
+ this.config.trustCookieValue
142
+ );
143
+
144
+ if (!this.deviceId || !this.userId) {
145
+ const discovered = await discoverDevice(cookieHeader);
146
+ this.deviceId = discovered.deviceId;
147
+ this.userId = discovered.userId;
148
+ this.log.info(`Discovered Withings device ${this.deviceId} for user ${this.userId}`);
149
+ if (this.infoService) {
150
+ this.infoService.setCharacteristic(this.Characteristic.SerialNumber, this.deviceId);
151
+ }
152
+ }
153
+
154
+ const { co2, temperature } = await fetchLatest({
155
+ cookieHeader,
156
+ sessionToken,
157
+ deviceId: this.deviceId,
158
+ userId: this.userId,
159
+ });
160
+
161
+ this.applyReading(co2, temperature);
162
+ this.setFault(false);
163
+ } catch (err) {
164
+ // Deliberately do not touch the value characteristics here — the Home app
165
+ // should keep showing the last known good reading, not go blank, when a
166
+ // poll fails (e.g. the trust cookie expired and login needs recapturing).
167
+ this.log.error(`Withings poll failed: ${err.message}`);
168
+ this.setFault(true);
169
+ }
170
+ }
171
+
172
+ applyReading(co2, temperature) {
173
+ const threshold = Number(this.config.co2DetectedThresholdPpm);
174
+ const co2Threshold = Number.isFinite(threshold) && threshold > 0 ? threshold : 1000;
175
+
176
+ if (co2 !== null && co2 !== undefined) {
177
+ this.co2Service.updateCharacteristic(this.Characteristic.CarbonDioxideLevel, co2);
178
+ this.co2Service.updateCharacteristic(
179
+ this.Characteristic.CarbonDioxideDetected,
180
+ co2 > co2Threshold
181
+ ? this.Characteristic.CarbonDioxideDetected.CO2_LEVELS_ABNORMAL
182
+ : this.Characteristic.CarbonDioxideDetected.CO2_LEVELS_NORMAL
183
+ );
184
+ this.airQualityService.updateCharacteristic(
185
+ this.Characteristic.AirQuality,
186
+ mapCo2ToAirQuality(co2, this.Characteristic.AirQuality)
187
+ );
188
+ }
189
+
190
+ if (temperature !== null && temperature !== undefined) {
191
+ this.temperatureService.updateCharacteristic(this.Characteristic.CurrentTemperature, temperature);
192
+ }
193
+ }
194
+
195
+ setFault(hasFault) {
196
+ const value = hasFault
197
+ ? this.Characteristic.StatusFault.GENERAL_FAULT
198
+ : this.Characteristic.StatusFault.NO_FAULT;
199
+
200
+ this.co2Service.updateCharacteristic(this.Characteristic.StatusFault, value);
201
+ this.airQualityService.updateCharacteristic(this.Characteristic.StatusFault, value);
202
+ this.temperatureService.updateCharacteristic(this.Characteristic.StatusFault, value);
203
+ }
204
+ }
205
+
206
+ module.exports = (api) => {
207
+ api.registerPlatform(PLATFORM_NAME, WithingsEnvironmentDataPlatform);
208
+ };
@@ -0,0 +1,39 @@
1
+ const ASSOCIATION_URL = 'https://scalews.withings.com/cgi-bin/association';
2
+
3
+ // Reverse-engineered/unofficial: not part of the documented Withings API.
4
+ // Lets the plugin find the account's scale deviceId/userId automatically
5
+ // after login, instead of requiring the user to hunt for them in DevTools.
6
+ async function discoverDevice(cookieHeader) {
7
+ const body = new URLSearchParams({ action: 'getbyaccountid', enrich: 't' }).toString();
8
+
9
+ const response = await fetch(ASSOCIATION_URL, {
10
+ method: 'POST',
11
+ headers: {
12
+ 'Content-Type': 'text/plain;charset=UTF-8',
13
+ Origin: 'https://app.withings.com',
14
+ Referer: 'https://app.withings.com/',
15
+ Cookie: cookieHeader,
16
+ },
17
+ body,
18
+ });
19
+
20
+ if (!response.ok) {
21
+ throw new Error(`Device discovery request failed with HTTP status ${response.status}`);
22
+ }
23
+
24
+ const json = await response.json();
25
+ if (json.status !== 0) {
26
+ throw new Error(`Device discovery returned Withings status ${json.status}`);
27
+ }
28
+
29
+ const deviceId = json.body.associations?.[0]?.deviceid;
30
+ const userId = json.body.accounts?.[0]?.users?.[0]?.userid;
31
+
32
+ if (!deviceId || !userId) {
33
+ throw new Error('Could not determine deviceId/userId from the Withings association response');
34
+ }
35
+
36
+ return { deviceId: String(deviceId), userId: String(userId) };
37
+ }
38
+
39
+ module.exports = { discoverDevice };
package/lib/login.js ADDED
@@ -0,0 +1,142 @@
1
+ const ACCOUNT_BASE = 'https://account.withings.com';
2
+ const USER_AGENT =
3
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36';
4
+
5
+ class CookieJar {
6
+ constructor() {
7
+ this.cookies = new Map();
8
+ }
9
+
10
+ setFromResponse(response) {
11
+ const setCookieHeaders =
12
+ typeof response.headers.getSetCookie === 'function'
13
+ ? response.headers.getSetCookie()
14
+ : response.headers.get('set-cookie')
15
+ ? [response.headers.get('set-cookie')]
16
+ : [];
17
+ for (const header of setCookieHeaders) {
18
+ const pair = header.split(';', 1)[0];
19
+ const eqIdx = pair.indexOf('=');
20
+ if (eqIdx === -1) continue;
21
+ const name = pair.slice(0, eqIdx).trim();
22
+ const value = pair.slice(eqIdx + 1).trim();
23
+ // Withings clears cookies by setting value to the literal string "deleted"
24
+ // (alongside an expired Expires attribute) rather than omitting them.
25
+ if (value === 'deleted' || /expires=[^;]*1970/i.test(header)) {
26
+ this.cookies.delete(name);
27
+ } else {
28
+ this.cookies.set(name, value);
29
+ }
30
+ }
31
+ }
32
+
33
+ set(name, value) {
34
+ this.cookies.set(name, value);
35
+ }
36
+
37
+ toHeader() {
38
+ return Array.from(this.cookies.entries())
39
+ .map(([name, value]) => `${name}=${value}`)
40
+ .join('; ');
41
+ }
42
+
43
+ findByPrefix(prefix) {
44
+ for (const [name, value] of this.cookies) {
45
+ if (name.startsWith(prefix)) return { name, value };
46
+ }
47
+ return null;
48
+ }
49
+ }
50
+
51
+ async function requestFollowingRedirects(method, url, body, jar, maxHops = 10) {
52
+ let currentUrl = url;
53
+ let currentMethod = method;
54
+ let currentBody = body;
55
+
56
+ for (let hop = 0; hop <= maxHops; hop++) {
57
+ const headers = {
58
+ Cookie: jar.toHeader(),
59
+ 'User-Agent': USER_AGENT,
60
+ Origin: ACCOUNT_BASE,
61
+ Referer: currentUrl,
62
+ };
63
+ if (currentBody) headers['Content-Type'] = 'application/x-www-form-urlencoded';
64
+
65
+ const response = await fetch(currentUrl, {
66
+ method: currentMethod,
67
+ headers,
68
+ body: currentBody,
69
+ redirect: 'manual',
70
+ });
71
+ jar.setFromResponse(response);
72
+
73
+ if (response.status >= 300 && response.status < 400) {
74
+ const location = response.headers.get('location');
75
+ if (!location) return { response, finalUrl: currentUrl };
76
+ currentUrl = new URL(location, currentUrl).toString();
77
+ currentMethod = 'GET';
78
+ currentBody = undefined;
79
+ continue;
80
+ }
81
+
82
+ if (response.status >= 400) {
83
+ throw new Error(
84
+ `Withings login request to ${currentUrl} failed with status ${response.status}`
85
+ );
86
+ }
87
+
88
+ return { response, finalUrl: currentUrl };
89
+ }
90
+
91
+ throw new Error(`Withings login exceeded ${maxHops} redirect hops starting from ${url}`);
92
+ }
93
+
94
+ async function login(email, password, trustCookieName, trustCookieValue) {
95
+ const jar = new CookieJar();
96
+ // This cookie (scoped to account.withings.com) is the "this device already passed
97
+ // 2FA" marker — its name has stayed stable across many logins. It must be sent on
98
+ // the very first request of a fresh login attempt, before email/password. Confirmed
99
+ // empirically that no other cookie (e.g. w_uuid) is needed alongside it.
100
+ jar.set(trustCookieName, trustCookieValue);
101
+
102
+ await requestFollowingRedirects('GET', `${ACCOUNT_BASE}/`, undefined, jar);
103
+
104
+ const emailBody = new URLSearchParams({ email }).toString();
105
+ await requestFollowingRedirects(
106
+ 'POST',
107
+ `${ACCOUNT_BASE}/new_workflow/login`,
108
+ emailBody,
109
+ jar
110
+ );
111
+
112
+ const passwordBody = new URLSearchParams({ password }).toString();
113
+ const { finalUrl } = await requestFollowingRedirects(
114
+ 'POST',
115
+ `${ACCOUNT_BASE}/new_workflow/password_check`,
116
+ passwordBody,
117
+ jar
118
+ );
119
+
120
+ if (finalUrl.includes('confirm_totp')) {
121
+ throw new Error(
122
+ "Withings session not trusted (landed on confirm_totp) — log in manually at " +
123
+ "https://account.withings.com with 'trust this device' checked during the 2FA step, " +
124
+ 'then update the trustCookieValue plugin config field with a fresh value ' +
125
+ '(DevTools -> Application tab -> Cookies -> account.withings.com).'
126
+ );
127
+ }
128
+
129
+ const sessionTokenCookie = jar.findByPrefix('2fa_token_');
130
+ if (!sessionTokenCookie) {
131
+ throw new Error(
132
+ 'Withings login completed but no 2fa_token_* cookie was found — the login flow may have changed.'
133
+ );
134
+ }
135
+
136
+ return {
137
+ cookieHeader: jar.toHeader(),
138
+ sessionToken: sessionTokenCookie.value,
139
+ };
140
+ }
141
+
142
+ module.exports = { login };
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "homebridge-withings-environment-data",
3
+ "version": "0.1.3",
4
+ "description": "Homebridge plugin exposing ambient CO2/air-quality and room temperature readings from a Withings WS-50 scale as HomeKit sensors",
5
+ "main": "index.js",
6
+ "keywords": [
7
+ "homebridge-plugin"
8
+ ],
9
+ "engines": {
10
+ "homebridge": ">=1.6.0",
11
+ "node": ">=18"
12
+ },
13
+ "license": "MIT"
14
+ }