homebridge-mynest 2.0.0 → 2.0.2
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 +14 -0
- package/README.md +48 -58
- package/config.schema.json +1 -3
- package/dist/accessories/global-eco.js +6 -0
- package/dist/accessories/thermostat.js +57 -6
- package/dist/api/thermostat-write.d.ts +9 -0
- package/dist/api/thermostat-write.js +9 -0
- package/dist/diagnostics/format.js +0 -4
- package/dist/platform.js +7 -9
- package/dist/settings.d.ts +9 -1
- package/dist/settings.js +9 -1
- package/docs/AUTH.md +28 -0
- package/docs/PROTOCOL.md +103 -0
- package/docs/README-DETAILED.md +135 -0
- package/package.json +12 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here. From the first release onward, [release-please](https://github.com/googleapis/release-please) owns this file — do not hand-edit version headings after `0.1.0` ships.
|
|
4
4
|
|
|
5
|
+
## [2.0.2](https://github.com/tbaur/homebridge-mynest/compare/v2.0.1...v2.0.2) (2026-10-05)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* log one diagnostics health warning ([ede8013](https://github.com/tbaur/homebridge-mynest/commit/ede8013cccdf0cf31396755e03a04a2ce7b96966))
|
|
11
|
+
|
|
12
|
+
## [2.0.1](https://github.com/tbaur/homebridge-mynest/compare/v2.0.0...v2.0.1) (2026-09-05)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* clamp published thermostat setpoints and report failed writes ([#46](https://github.com/tbaur/homebridge-mynest/issues/46)) ([c5bf879](https://github.com/tbaur/homebridge-mynest/commit/c5bf87993934dd17d43653bceb296c674a3fd7cc))
|
|
18
|
+
|
|
5
19
|
## [2.0.0](https://github.com/tbaur/homebridge-mynest/compare/v1.1.0...v2.0.0) (2026-08-05)
|
|
6
20
|
|
|
7
21
|
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://github.com/tbaur/homebridge-mynest/actions/workflows/test.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/homebridge-mynest)
|
|
5
5
|
[](https://www.npmjs.com/package/homebridge-mynest)
|
|
6
|
-
[](https://nodejs.org)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
7
|
[](https://homebridge.io)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
|
|
@@ -12,32 +12,27 @@ Expose Nest thermostats, Nest Protect smoke/CO alarms, and Nest Temperature Sens
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
14
|
### Device Support
|
|
15
|
-
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- **Occupancy is not motion** — Nest publishes `auto_away` for mains-powered Protects: roughly a ten-minute presence verdict. Battery Protects, power-unknown Protects, and Observe-only Protects get no occupancy sensor rather than a stuck "empty house"; when REST later goes stale, a last occupancy reading stays published but is marked inactive/faulted
|
|
24
|
-
- **Smoke/CO honesty without accessory churn** — Observe-only Protects omit smoke/CO until REST reports alarm state; if REST later goes down, tiles stay in HomeKit (rooms/automations keep their targets) but are marked inactive/faulted rather than freezing a live all-clear. Battery and online may remain from Observe
|
|
25
|
-
- **Thermostats are Observe-first** — Modern Nest thermostats may be missing from REST entirely; this plugin does not rely on REST-only discovery for HVAC
|
|
15
|
+
- **Thermostats:** temperature, mode, activity, setpoints, humidity when Nest reports it, plus an Eco Mode switch per thermostat
|
|
16
|
+
- **Optional house-wide Eco switch:** `exposeGlobalEcoSwitch`
|
|
17
|
+
- **Thermostat writes:** mode, setpoints, and Eco only when **Allow thermostat control** is on (off by default)
|
|
18
|
+
- **Nest Protect:** smoke and CO from REST when available; battery and online from REST and/or Observe
|
|
19
|
+
- **Optional Protect occupancy and temperature**
|
|
20
|
+
- **Temperature Sensors:** Nest Temperature Sensor (kryptonite) pucks
|
|
21
|
+
- **Dual transport:** Nest REST subscribe merged with HTTP/2 Observe, so Observe-only devices still appear
|
|
26
22
|
|
|
27
23
|
### Reliability
|
|
28
|
-
|
|
29
|
-
- **
|
|
30
|
-
- **
|
|
31
|
-
- **Diagnostics** *(optional)* — Opt-in health/activity heartbeats, boot/shutdown snapshots, and healthy/degraded transitions in the Homebridge log (REST + Observe transport gauges, breaker state, device inventory, API latency)
|
|
24
|
+
- **No accessory churn** on a cloud blip or expired token. Devices leave HomeKit only after Nest-confirmed drops
|
|
25
|
+
- **Circuit breakers** on REST and Observe so a dead Nest edge fails fast
|
|
26
|
+
- **Diagnostics** *(optional):* health heartbeats in the Homebridge log, with one warning when health becomes degraded
|
|
32
27
|
|
|
33
28
|
### Quality
|
|
34
|
-
|
|
35
29
|
<!-- Canonical test count lives here only; keep other docs number-free to avoid multi-place updates. -->
|
|
36
|
-
- **
|
|
37
|
-
- **
|
|
38
|
-
- **
|
|
39
|
-
- **
|
|
40
|
-
|
|
30
|
+
- **576 tests** with a per-area coverage floor, and a CI step that fails if the suite leaks an open handle
|
|
31
|
+
- **Built for Homebridge 2:** stored getters and `updateValue(...)`, not the removed `getValue()`
|
|
32
|
+
- **Secret hygiene:** access tokens are redacted from logs
|
|
33
|
+
- **No analytics**
|
|
34
|
+
|
|
35
|
+
Every option, occupancy caveat, and troubleshooting step is in [Detailed documentation](docs/README-DETAILED.md).
|
|
41
36
|
|
|
42
37
|
## Quick Start
|
|
43
38
|
|
|
@@ -45,17 +40,15 @@ Expose Nest thermostats, Nest Protect smoke/CO alarms, and Nest Temperature Sens
|
|
|
45
40
|
|
|
46
41
|
**Homebridge UI** (recommended): Plugins → Search `mynest` → Install
|
|
47
42
|
|
|
48
|
-
**Command line:**
|
|
49
|
-
|
|
50
43
|
```bash
|
|
51
44
|
npm install -g homebridge-mynest
|
|
52
45
|
```
|
|
53
46
|
|
|
54
47
|
### 2. Get a Nest Account token
|
|
55
48
|
|
|
56
|
-
You need a Nest Account (not Google-only) access token from [home.nest.com/session](https://home.nest.com/session). Steps and threat model: [docs/AUTH.md](
|
|
49
|
+
You need a Nest Account (not Google-only) access token from [home.nest.com/session](https://home.nest.com/session). Steps and threat model: [docs/AUTH.md](docs/AUTH.md).
|
|
57
50
|
|
|
58
|
-
|
|
51
|
+
Treat that token like a password. Nest Account sessions are account-scoped credentials.
|
|
59
52
|
|
|
60
53
|
### 3. Configure
|
|
61
54
|
|
|
@@ -77,14 +70,14 @@ Use the Homebridge UI, or add the platform to `config.json`:
|
|
|
77
70
|
|
|
78
71
|
Thermostats, Protects, and temperature sensors appear in the Home app as Nest reports them. The log should show `Connected to Nest (REST up; Observe connecting)`, then device adds, then `Platform ready`.
|
|
79
72
|
|
|
80
|
-
Thermostat control
|
|
73
|
+
Thermostat control stays off until you enable **Allow thermostat control**. Optionally enable **Expose global Eco switch**.
|
|
81
74
|
|
|
82
75
|
## Supported Devices
|
|
83
76
|
|
|
84
77
|
| Nest device | HomeKit accessory | Notes |
|
|
85
78
|
| --- | --- | --- |
|
|
86
|
-
| Thermostat | Thermostat + Eco Mode switch | Observe is source of truth
|
|
87
|
-
| Nest Protect | Smoke + CO (+ optional occupancy / temp) | Smoke/CO require REST `topaz
|
|
79
|
+
| Thermostat | Thermostat + Eco Mode switch | Observe is source of truth. Writes need `allowThermostatControl`. |
|
|
80
|
+
| Nest Protect | Smoke + CO (+ optional occupancy / temp) | Smoke/CO require REST `topaz`. Occupancy is ~10-minute presence, not motion. |
|
|
88
81
|
| Temperature Sensor | Temperature Sensor | Battery + temperature |
|
|
89
82
|
|
|
90
83
|
Cameras, doorbells, Yale locks, Home/Away structure switches, and Google-account-only homes are out of scope.
|
|
@@ -95,54 +88,51 @@ Cameras, doorbells, Yale locks, Home/Away structure switches, and Google-account
|
|
|
95
88
|
|
|
96
89
|
| Option | Default | Description |
|
|
97
90
|
| --- | --- | --- |
|
|
98
|
-
| `name` | `MyNest` | Required. Plugin instance name
|
|
99
|
-
| `accessToken` | — | Required. Nest Account `access_token
|
|
100
|
-
| `allowThermostatControl` | `false` |
|
|
101
|
-
| `exposeGlobalEcoSwitch` | `false` |
|
|
102
|
-
| `exposeProtectOccupancy` | `true` | Occupancy from REST `auto_away`
|
|
91
|
+
| `name` | `MyNest` | Required. Plugin instance name in Homebridge logs. |
|
|
92
|
+
| `accessToken` | — | Required. Nest Account `access_token`. See [docs/AUTH.md](docs/AUTH.md). |
|
|
93
|
+
| `allowThermostatControl` | `false` | Send mode/setpoint/Eco changes to Nest. |
|
|
94
|
+
| `exposeGlobalEcoSwitch` | `false` | One switch that sets Eco on every thermostat. Writes still need `allowThermostatControl`. |
|
|
95
|
+
| `exposeProtectOccupancy` | `true` | Occupancy from REST `auto_away` on a mains-powered Protect. |
|
|
103
96
|
| `exposeProtectTemperature` | `false` | Temperature/humidity measured by each Protect. |
|
|
104
97
|
| `ignoredDeviceIds` | `[]` | Device IDs or serials to leave out of HomeKit. |
|
|
105
98
|
| `fieldTest` | `false` | Use Nest field-test hosts. |
|
|
106
|
-
| `diagnosticsInterval` | `0` | Seconds between health heartbeats
|
|
107
|
-
| `structuredLogs` | `false` |
|
|
99
|
+
| `diagnosticsInterval` | `0` | Seconds between health heartbeats; `0` off, else `30`–`86400`. |
|
|
100
|
+
| `structuredLogs` | `false` | With diagnostics, also emit a JSON line. |
|
|
108
101
|
| `debug` | `false` | Verbose logging; tokens are redacted. |
|
|
109
102
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
Nest does not expose a reliable Protect motion event stream to third-party clients. What this plugin publishes (when REST `auto_away` exists on a mains-powered Protect) is Nest's own presence hold-off — on the order of **ten minutes** after the room empties, clearing when activity is seen again. Do not build automations that expect pathlight-speed motion.
|
|
103
|
+
The [detailed documentation](docs/README-DETAILED.md#full-configuration-reference) explains each option and what happens when REST or Observe is down.
|
|
113
104
|
|
|
114
105
|
## Not Working?
|
|
115
106
|
|
|
116
|
-
1. **Authentication error
|
|
117
|
-
2. **Thermostat missing
|
|
118
|
-
3. **
|
|
119
|
-
4. **
|
|
120
|
-
5. **
|
|
121
|
-
6. **No occupancy** — Battery Protect, power unknown, Observe-only Protect, or `exposeProtectOccupancy` off.
|
|
107
|
+
1. **Authentication error:** token missing, truncated, Google JWT/`ya29.`, or revoked. Capture a fresh Nest Account token ([docs/AUTH.md](docs/AUTH.md)).
|
|
108
|
+
2. **Thermostat missing:** modern thermostats are Observe-only on some accounts. Enable `debug` for stream detail.
|
|
109
|
+
3. **Eco / setpoint changes snap back:** enable **Allow thermostat control**.
|
|
110
|
+
4. **Protect without smoke/CO:** likely Observe-only. The accessory still appears; alarm tiles wait for REST.
|
|
111
|
+
5. **No occupancy:** battery Protect, power unknown, Observe-only, or `exposeProtectOccupancy` off. Occupancy is not motion.
|
|
122
112
|
|
|
123
|
-
|
|
113
|
+
The [full troubleshooting list](docs/README-DETAILED.md#troubleshooting) covers more cases, including a stuck Home app tile.
|
|
124
114
|
|
|
125
|
-
|
|
115
|
+
## Security
|
|
126
116
|
|
|
127
|
-
|
|
117
|
+
This plugin holds a Nest Account `access_token` in Homebridge's plaintext `config.json`. Anyone who can read that file can act as the Nest web app for the home. Secure the host, rotate the token if it may have leaked, and never paste tokens into issues or logs.
|
|
128
118
|
|
|
129
|
-
|
|
119
|
+
See [SECURITY.md](SECURITY.md) and [docs/AUTH.md](docs/AUTH.md).
|
|
130
120
|
|
|
131
121
|
## Requirements
|
|
132
122
|
|
|
133
|
-
- **Homebridge 2.x**
|
|
134
|
-
- **Node.js 22
|
|
123
|
+
- **Homebridge 2.x** (Homebridge 1.x is not supported)
|
|
124
|
+
- **Node.js 22, 24, or 26**
|
|
135
125
|
- A Nest Account (not Google-only) with an `access_token` from [home.nest.com/session](https://home.nest.com/session)
|
|
136
126
|
|
|
137
127
|
## More Info
|
|
138
128
|
|
|
139
|
-
- [
|
|
140
|
-
- [
|
|
141
|
-
- [
|
|
142
|
-
- [
|
|
143
|
-
- [
|
|
144
|
-
- [Security policy](
|
|
145
|
-
- [Changelog](
|
|
129
|
+
- [Detailed documentation](docs/README-DETAILED.md)
|
|
130
|
+
- [Authentication](docs/AUTH.md)
|
|
131
|
+
- [Protocol notes](docs/PROTOCOL.md)
|
|
132
|
+
- [Development](DEVELOPMENT.md)
|
|
133
|
+
- [Contributing](CONTRIBUTING.md)
|
|
134
|
+
- [Security policy](SECURITY.md)
|
|
135
|
+
- [Changelog](CHANGELOG.md)
|
|
146
136
|
- [Report issues](https://github.com/tbaur/homebridge-mynest/issues)
|
|
147
137
|
|
|
148
138
|
## License
|
package/config.schema.json
CHANGED
|
@@ -6,18 +6,17 @@
|
|
|
6
6
|
"footerDisplay": "Thermostat mode/setpoints/Eco write through Nest BatchUpdateState only when Allow thermostat control is enabled (off by default). Each thermostat gets an Eco Mode switch; optionally enable Expose global Eco switch for all-at-once. Protect occupancy is Nest's ~10-minute presence verdict (auto_away), not motion. Treat the access token like a password.",
|
|
7
7
|
"schema": {
|
|
8
8
|
"type": "object",
|
|
9
|
+
"required": ["name", "accessToken"],
|
|
9
10
|
"properties": {
|
|
10
11
|
"name": {
|
|
11
12
|
"title": "Name",
|
|
12
13
|
"type": "string",
|
|
13
14
|
"default": "MyNest",
|
|
14
|
-
"required": true,
|
|
15
15
|
"description": "Name shown in the Homebridge log."
|
|
16
16
|
},
|
|
17
17
|
"accessToken": {
|
|
18
18
|
"title": "Nest access token",
|
|
19
19
|
"type": "string",
|
|
20
|
-
"required": true,
|
|
21
20
|
"minLength": 32,
|
|
22
21
|
"x-schema-form": {
|
|
23
22
|
"type": "password"
|
|
@@ -52,7 +51,6 @@
|
|
|
52
51
|
"title": "Ignored device IDs",
|
|
53
52
|
"type": "array",
|
|
54
53
|
"default": [],
|
|
55
|
-
"required": false,
|
|
56
54
|
"description": "Nest device IDs or serial numbers to leave out of HomeKit. IDs appear in the Homebridge log when devices are added.",
|
|
57
55
|
"items": {
|
|
58
56
|
"type": "string",
|
|
@@ -148,6 +148,12 @@ class GlobalEcoAccessory {
|
|
|
148
148
|
this.#clearPending();
|
|
149
149
|
this.#log.warn(`${settings_1.GLOBAL_ECO_DISPLAY_NAME}: Eco update failed: ${(0, sanitizers_1.sanitizeError)(error)}`);
|
|
150
150
|
setImmediate(() => this.#binder.refresh());
|
|
151
|
+
// Same distinction the thermostat draws: a refusal above resolves, but a
|
|
152
|
+
// write that reached Nest and failed is reported so the user is not left
|
|
153
|
+
// watching a switch flip back with no stated reason. The deferred refresh
|
|
154
|
+
// clears HAP's error status a tick later, so it cannot stick.
|
|
155
|
+
const { HAPStatus, HapStatusError } = this.#platform.api.hap;
|
|
156
|
+
throw new HapStatusError(-70402 /* HAPStatus.SERVICE_COMMUNICATION_FAILURE */);
|
|
151
157
|
}
|
|
152
158
|
}
|
|
153
159
|
#applyAccessoryInformation() {
|
|
@@ -25,6 +25,23 @@ function midpoint(low, high) {
|
|
|
25
25
|
}
|
|
26
26
|
return (low + high) / 2;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* A Nest setpoint on the grid this accessory publishes.
|
|
30
|
+
*
|
|
31
|
+
* HAP quantizes and clamps whatever it is handed against the characteristic's
|
|
32
|
+
* props, so pushing Nest's raw Celsius leaves HomeKit holding a number the
|
|
33
|
+
* plugin never computed. A thermostat set to Fahrenheit makes that the normal
|
|
34
|
+
* case rather than an edge one: it stores 72 °F as 22.222 °C, and every push
|
|
35
|
+
* lands a quarter degree from what the Nest app shows. Doing the arithmetic
|
|
36
|
+
* here keeps `onGet`, plugin state, and HomeKit's cache on one number.
|
|
37
|
+
*
|
|
38
|
+
* Quantize first and anchor the grid at {@link MIN_SETPOINT_C}, because that is
|
|
39
|
+
* what HAP does with `minStep` and `minValue`.
|
|
40
|
+
*/
|
|
41
|
+
function toPublishedSetpoint(celsius) {
|
|
42
|
+
const steps = Math.round((celsius - settings_1.MIN_SETPOINT_C) / settings_1.SETPOINT_STEP_C);
|
|
43
|
+
return (0, thermostat_write_1.clampSetpoint)(steps * settings_1.SETPOINT_STEP_C + settings_1.MIN_SETPOINT_C);
|
|
44
|
+
}
|
|
28
45
|
class ThermostatAccessory extends base_1.NestAccessory {
|
|
29
46
|
#service;
|
|
30
47
|
#ecoService = null;
|
|
@@ -120,9 +137,18 @@ class ThermostatAccessory extends base_1.NestAccessory {
|
|
|
120
137
|
* Send a Nest write when control is enabled; otherwise refresh so HomeKit
|
|
121
138
|
* does not keep a slider position Nest never accepted.
|
|
122
139
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
140
|
+
* A refused write and a failed one are different events and must not look
|
|
141
|
+
* alike. Refused means `allowThermostatControl` is off: nothing malfunctioned,
|
|
142
|
+
* the plugin is doing what it was configured to do, so `onSet` resolves and
|
|
143
|
+
* the last good values are pushed back.
|
|
144
|
+
*
|
|
145
|
+
* A write that reached Nest and failed is reported as a HAP communication
|
|
146
|
+
* failure, so the user learns their change was dropped instead of watching a
|
|
147
|
+
* slider spring back for no stated reason — and an automation awaiting the
|
|
148
|
+
* write sees it fail rather than succeed. Safe here only because every
|
|
149
|
+
* characteristic is bound with an `onGet` handler and the deferred refresh
|
|
150
|
+
* pushes a value a tick later: HAP resets the error status on both paths, so
|
|
151
|
+
* the failure cannot settle into a sticky "No Response".
|
|
126
152
|
*/
|
|
127
153
|
async #write(patch) {
|
|
128
154
|
try {
|
|
@@ -137,6 +163,11 @@ class ThermostatAccessory extends base_1.NestAccessory {
|
|
|
137
163
|
catch (error) {
|
|
138
164
|
this.log.warn(`${this.identity.name}: thermostat update failed: ${(0, sanitizers_1.sanitizeError)(error)}`);
|
|
139
165
|
this.#revertHomeKitValues();
|
|
166
|
+
// `HapStatusError` rather than the Nest error: rethrowing that would put a
|
|
167
|
+
// stack trace in the Homebridge log through HAP's unhandled-error path,
|
|
168
|
+
// and Nest's message is already logged above without the token in it.
|
|
169
|
+
const { HAPStatus, HapStatusError } = this.platform.api.hap;
|
|
170
|
+
throw new HapStatusError(-70402 /* HAPStatus.SERVICE_COMMUNICATION_FAILURE */);
|
|
140
171
|
}
|
|
141
172
|
}
|
|
142
173
|
/**
|
|
@@ -179,6 +210,11 @@ class ThermostatAccessory extends base_1.NestAccessory {
|
|
|
179
210
|
},
|
|
180
211
|
});
|
|
181
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Send an Eco write, drawing the same line between refused and failed as
|
|
215
|
+
* {@link #write}: a refusal is the configured behaviour and resolves, while a
|
|
216
|
+
* write that reached Nest and failed is reported as a communication failure.
|
|
217
|
+
*/
|
|
182
218
|
async #writeEco(ecoOn) {
|
|
183
219
|
try {
|
|
184
220
|
const sent = await this.platform.applyEcoWrite(this.deviceId, ecoOn);
|
|
@@ -192,6 +228,8 @@ class ThermostatAccessory extends base_1.NestAccessory {
|
|
|
192
228
|
catch (error) {
|
|
193
229
|
this.log.warn(`${this.identity.name}: Eco update failed: ${(0, sanitizers_1.sanitizeError)(error)}`);
|
|
194
230
|
this.#revertHomeKitValues();
|
|
231
|
+
const { HAPStatus, HapStatusError } = this.platform.api.hap;
|
|
232
|
+
throw new HapStatusError(-70402 /* HAPStatus.SERVICE_COMMUNICATION_FAILURE */);
|
|
195
233
|
}
|
|
196
234
|
}
|
|
197
235
|
/**
|
|
@@ -230,7 +268,7 @@ class ThermostatAccessory extends base_1.NestAccessory {
|
|
|
230
268
|
const characteristic = this.binder.bind(this.#service, type, () => {
|
|
231
269
|
const value = read();
|
|
232
270
|
if (value !== undefined) {
|
|
233
|
-
return value;
|
|
271
|
+
return toPublishedSetpoint(value);
|
|
234
272
|
}
|
|
235
273
|
const current = this.#service.getCharacteristic(type).value;
|
|
236
274
|
return typeof current === 'number'
|
|
@@ -240,8 +278,21 @@ class ThermostatAccessory extends base_1.NestAccessory {
|
|
|
240
278
|
? current
|
|
241
279
|
: settings_1.MIN_SETPOINT_C;
|
|
242
280
|
}, options.write ? { write: options.write } : {});
|
|
243
|
-
|
|
244
|
-
|
|
281
|
+
// Correct both bounds; only the floor used to be. `setProps` clamps a
|
|
282
|
+
// cached value into the range itself, but it reports doing so, and Heating
|
|
283
|
+
// Threshold arrives holding HAP's default of 0 °C on every fresh
|
|
284
|
+
// thermostat, so pre-correcting is what keeps that off the log.
|
|
285
|
+
//
|
|
286
|
+
// Push only when the clamp actually moves the value. `updateValue` runs
|
|
287
|
+
// before the props below and is validated against the ones still in force,
|
|
288
|
+
// whose Heating Threshold ceiling is 25 °C, so re-sending an already-legal
|
|
289
|
+
// 30 °C would push it down to 25 instead of leaving it alone.
|
|
290
|
+
const cached = characteristic.value;
|
|
291
|
+
if (typeof cached === 'number') {
|
|
292
|
+
const clamped = (0, thermostat_write_1.clampSetpoint)(cached);
|
|
293
|
+
if (clamped !== cached) {
|
|
294
|
+
characteristic.updateValue(clamped);
|
|
295
|
+
}
|
|
245
296
|
}
|
|
246
297
|
characteristic.setProps({
|
|
247
298
|
minValue: settings_1.MIN_SETPOINT_C,
|
|
@@ -65,3 +65,12 @@ export declare function formatThermostatUpdateLog(write: ThermostatSetpointWrite
|
|
|
65
65
|
export declare function encodeEcoModeBatchUpdate(resourceId: string, ecoOn: boolean): Buffer;
|
|
66
66
|
/** Encode a NestMessage suitable for TraitBatchApi/BatchUpdateState. */
|
|
67
67
|
export declare function encodeTargetTemperatureBatchUpdate(write: ThermostatSetpointWrite): Buffer;
|
|
68
|
+
/**
|
|
69
|
+
* Confine a Celsius setpoint to the range Nest accepts.
|
|
70
|
+
*
|
|
71
|
+
* Exported because the accessory's publish path needs the same bounds: the
|
|
72
|
+
* range HomeKit is told about and the range a write is clamped to must come
|
|
73
|
+
* from one place, or a value the plugin publishes can be one it would refuse
|
|
74
|
+
* to send back.
|
|
75
|
+
*/
|
|
76
|
+
export declare function clampSetpoint(celsius: number): number;
|
|
@@ -20,6 +20,7 @@ exports.buildThermostatSetpointWrite = buildThermostatSetpointWrite;
|
|
|
20
20
|
exports.formatThermostatUpdateLog = formatThermostatUpdateLog;
|
|
21
21
|
exports.encodeEcoModeBatchUpdate = encodeEcoModeBatchUpdate;
|
|
22
22
|
exports.encodeTargetTemperatureBatchUpdate = encodeTargetTemperatureBatchUpdate;
|
|
23
|
+
exports.clampSetpoint = clampSetpoint;
|
|
23
24
|
const node_crypto_1 = require("node:crypto");
|
|
24
25
|
const settings_1 = require("../settings");
|
|
25
26
|
const protobuf_1 = require("./protobuf");
|
|
@@ -223,6 +224,14 @@ function resolveStandbyMode(state, patchMode) {
|
|
|
223
224
|
}
|
|
224
225
|
return state.canCool && !state.canHeat ? 'cool' : 'heat';
|
|
225
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Confine a Celsius setpoint to the range Nest accepts.
|
|
229
|
+
*
|
|
230
|
+
* Exported because the accessory's publish path needs the same bounds: the
|
|
231
|
+
* range HomeKit is told about and the range a write is clamped to must come
|
|
232
|
+
* from one place, or a value the plugin publishes can be one it would refuse
|
|
233
|
+
* to send back.
|
|
234
|
+
*/
|
|
226
235
|
function clampSetpoint(celsius) {
|
|
227
236
|
return Math.min(settings_1.MAX_SETPOINT_C, Math.max(settings_1.MIN_SETPOINT_C, celsius));
|
|
228
237
|
}
|
|
@@ -18,10 +18,6 @@ function diagnosticLabel(msg) {
|
|
|
18
18
|
return 'Diagnostics start';
|
|
19
19
|
case 'diagnostics.stop':
|
|
20
20
|
return 'Diagnostics stop';
|
|
21
|
-
case 'health.degraded':
|
|
22
|
-
return 'Health degraded';
|
|
23
|
-
case 'health.recovered':
|
|
24
|
-
return 'Health recovered';
|
|
25
21
|
default:
|
|
26
22
|
return msg;
|
|
27
23
|
}
|
package/dist/platform.js
CHANGED
|
@@ -884,22 +884,20 @@ class MyNestPlatform {
|
|
|
884
884
|
this.#diagnosticsTimer = setInterval(() => this.#diagnosticsHeartbeat(), interval);
|
|
885
885
|
this.#diagnosticsTimer.unref?.();
|
|
886
886
|
}
|
|
887
|
+
/**
|
|
888
|
+
* Emits one heartbeat. The line is a warning on the interval where health
|
|
889
|
+
* flips from healthy to degraded, and info otherwise. A reader failure stays
|
|
890
|
+
* inside the timer so it cannot crash Homebridge.
|
|
891
|
+
*/
|
|
887
892
|
#diagnosticsHeartbeat() {
|
|
888
893
|
if (!this.#diagnostics) {
|
|
889
894
|
return;
|
|
890
895
|
}
|
|
891
896
|
try {
|
|
892
897
|
const report = this.#diagnostics.buildHeartbeat(this.#buildDiagnosticsReaders());
|
|
893
|
-
this.#emitDiagnostic('info', report);
|
|
894
898
|
const health = report.lifecycle.health;
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
const transition = {
|
|
898
|
-
...report,
|
|
899
|
-
msg: isDegraded ? 'health.degraded' : 'health.recovered',
|
|
900
|
-
};
|
|
901
|
-
this.#emitDiagnostic(isDegraded ? 'warn' : 'info', transition);
|
|
902
|
-
}
|
|
899
|
+
const becameDegraded = this.#lastDiagnosticsHealth === 'healthy' && health === 'degraded';
|
|
900
|
+
this.#emitDiagnostic(becameDegraded ? 'warn' : 'info', report);
|
|
903
901
|
this.#lastDiagnosticsHealth = health;
|
|
904
902
|
}
|
|
905
903
|
catch (error) {
|
package/dist/settings.d.ts
CHANGED
|
@@ -336,7 +336,15 @@ export declare const MAX_DIAGNOSTICS_INTERVAL_SEC = 86400;
|
|
|
336
336
|
export declare const PROTECT_OCCUPANCY_HOLD_OFF_SEC = 600;
|
|
337
337
|
/** Floor on the thermostat setpoint HomeKit may request, in Celsius. */
|
|
338
338
|
export declare const MIN_SETPOINT_C = 9;
|
|
339
|
-
/**
|
|
339
|
+
/**
|
|
340
|
+
* Ceiling on the thermostat setpoint HomeKit may request, in Celsius.
|
|
341
|
+
*
|
|
342
|
+
* Nest's own ceiling is 90 °F, or 32.222 °C, which does not sit on the
|
|
343
|
+
* {@link SETPOINT_STEP_C} grid. HAP quantizes a value onto that grid *before*
|
|
344
|
+
* range-checking it, so 32.222 arrives as 32.0 and needs no headroom above 32
|
|
345
|
+
* to publish cleanly. Raising this to 32.5 would only add a setpoint the Home
|
|
346
|
+
* app can offer and Nest cannot honour.
|
|
347
|
+
*/
|
|
340
348
|
export declare const MAX_SETPOINT_C = 32;
|
|
341
349
|
/** Granularity of thermostat setpoints in Celsius, matching Nest's own UI. */
|
|
342
350
|
export declare const SETPOINT_STEP_C = 0.5;
|
package/dist/settings.js
CHANGED
|
@@ -362,7 +362,15 @@ exports.MAX_DIAGNOSTICS_INTERVAL_SEC = 86_400;
|
|
|
362
362
|
exports.PROTECT_OCCUPANCY_HOLD_OFF_SEC = 600;
|
|
363
363
|
/** Floor on the thermostat setpoint HomeKit may request, in Celsius. */
|
|
364
364
|
exports.MIN_SETPOINT_C = 9;
|
|
365
|
-
/**
|
|
365
|
+
/**
|
|
366
|
+
* Ceiling on the thermostat setpoint HomeKit may request, in Celsius.
|
|
367
|
+
*
|
|
368
|
+
* Nest's own ceiling is 90 °F, or 32.222 °C, which does not sit on the
|
|
369
|
+
* {@link SETPOINT_STEP_C} grid. HAP quantizes a value onto that grid *before*
|
|
370
|
+
* range-checking it, so 32.222 arrives as 32.0 and needs no headroom above 32
|
|
371
|
+
* to publish cleanly. Raising this to 32.5 would only add a setpoint the Home
|
|
372
|
+
* app can offer and Nest cannot honour.
|
|
373
|
+
*/
|
|
366
374
|
exports.MAX_SETPOINT_C = 32;
|
|
367
375
|
/** Granularity of thermostat setpoints in Celsius, matching Nest's own UI. */
|
|
368
376
|
exports.SETPOINT_STEP_C = 0.5;
|
package/docs/AUTH.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Authentication
|
|
2
|
+
|
|
3
|
+
Install and options: [README](../README.md). Full options and troubleshooting: [README-DETAILED.md](README-DETAILED.md).
|
|
4
|
+
|
|
5
|
+
This plugin supports **Nest Account** access tokens only. Google account sign-in (browser cookies, `issueToken`, `ya29.` OAuth tokens, JWTs) is intentionally not implemented.
|
|
6
|
+
|
|
7
|
+
Nest publishes no consumer API for this path. The plugin opens a session the same way the Nest web app does: `GET https://home.nest.com/session` with `Authorization: Basic <access_token>`, then uses the session `access_token`, `userid`, and `urls.transport_url` for REST and Observe.
|
|
8
|
+
|
|
9
|
+
## Getting the token
|
|
10
|
+
|
|
11
|
+
1. Sign in at [home.nest.com](https://home.nest.com) with a Nest Account (the pre-Google Nest login lineage).
|
|
12
|
+
2. In the same browser, open [https://home.nest.com/session](https://home.nest.com/session).
|
|
13
|
+
3. Copy the `access_token` field from the JSON response.
|
|
14
|
+
4. Paste it into the plugin's **Nest access token** field (or into `NEST_ACCESS_TOKEN` for `npm run verify`).
|
|
15
|
+
|
|
16
|
+
You want the Nest Account token from that session JSON — not a Google cookie, not a JWT (`eyJ…`), and not a `ya29.` OAuth token.
|
|
17
|
+
|
|
18
|
+
## Treat it as a password
|
|
19
|
+
|
|
20
|
+
Anyone with this token can control every thermostat and smoke alarm on the Nest Account. Homebridge stores it in plaintext in `config.json`.
|
|
21
|
+
|
|
22
|
+
- Do not paste it into issues, logs, screenshots, or chat.
|
|
23
|
+
- Prefer a Nest Account used only for Homebridge when that is practical.
|
|
24
|
+
- If it leaks, revoke Nest sessions / rotate credentials from Nest's account settings, then capture a fresh token.
|
|
25
|
+
|
|
26
|
+
When Nest rejects the token you will see an authentication error in the Homebridge log and the plugin will stop retrying until you fix the config and restart.
|
|
27
|
+
|
|
28
|
+
The wire protocol around the session is in [PROTOCOL.md](PROTOCOL.md). The threat model is in [SECURITY.md](../SECURITY.md).
|
package/docs/PROTOCOL.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Protocol notes
|
|
2
|
+
|
|
3
|
+
User-facing options and troubleshooting: [README-DETAILED.md](README-DETAILED.md). Token capture: [AUTH.md](AUTH.md).
|
|
4
|
+
|
|
5
|
+
Nest publishes no consumer API. Everything below was confirmed with a probe kit against a live Nest Account. Treat this as a working map of private backends, not a promise Nest will keep them stable.
|
|
6
|
+
|
|
7
|
+
## Dual transport
|
|
8
|
+
|
|
9
|
+
| Path | Role |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| REST `app_launch` + `/v5/subscribe` | Protect alarm state (`topaz`), structure/where, temperature sensors (`kryptonite`), older thermostat buckets when present |
|
|
12
|
+
| Observe (HTTP/2 gRPC-web protobuf) | Source of truth for modern thermostats; complete Protect inventory; trait patches for HVAC |
|
|
13
|
+
|
|
14
|
+
Neither transport is a superset of the other. On the account used to build this plugin, REST returned six Protects and zero thermostats while Observe returned seven Protects and five thermostats — and REST still claimed `num_thermostats: "5+"`. The plugin unions Observe ∪ REST and records which transport supplied each device.
|
|
15
|
+
|
|
16
|
+
**Critical merge rule:** Observe is authoritative for thermostats and for the complete Protect list. One Protect (post-reset) can be Observe-only and missing from REST `topaz`/`swarm`; it must still appear in HomeKit.
|
|
17
|
+
|
|
18
|
+
## Accessory removal
|
|
19
|
+
|
|
20
|
+
HomeKit accessories are unregistered only when Nest has confirmed a device is gone — never because a transport errored, a breaker opened, or a boot raced REST ahead of Observe.
|
|
21
|
+
|
|
22
|
+
| Signal | Behaviour |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| Failed REST / Observe request | Keep last inventory; no unregister |
|
|
25
|
+
| REST `app_launch` empty or under half prior size | Treat as truncated; merge present keys; keep missing |
|
|
26
|
+
| REST `app_launch` omits a key | Two consecutive complete omissions before drop |
|
|
27
|
+
| Observe reconnect empty or under half prior devices | Treat as incomplete; keep prior Observe state |
|
|
28
|
+
| Observe omits a `DEVICE_*` | Two consecutive complete snapshots before prune |
|
|
29
|
+
| Observe never connected this session | Do not unregister (REST-only view cannot prove thermostats are gone) |
|
|
30
|
+
| Device absent from the Observe ∪ REST inventory after the above | Unregister from HomeKit |
|
|
31
|
+
| Expired / revoked access token (or both transports exhausted on HTTP 403) | Stop updates; **keep** accessories — Nest Account auth is a manually pasted session token, so unregistering would bounce rooms/automations until the user pastes a fresh one and restarts |
|
|
32
|
+
|
|
33
|
+
Unusable plugin config at startup (missing/invalid token shape, etc.) **keeps** the cached accessories, for the same reason an expired token does. A typo, a half-edited `config.json`, or a truncated token would otherwise destroy every room assignment, scene membership, and automation target in the Home app — and fixing the config does not bring them back, because HomeKit treats the re-registered accessories as new devices. A config error cannot warrant a more destructive response than a revoked token. The plugin logs why and publishes nothing until it is restarted with a usable config.
|
|
34
|
+
|
|
35
|
+
A transient failure at startup (DNS, TLS, a timeout, a 5xx) is **not** fatal: it is retried with backoff, because `didFinishLaunching` fires only once and a host that boots before its network is up would otherwise stay idle for the lifetime of the process.
|
|
36
|
+
|
|
37
|
+
## Session
|
|
38
|
+
|
|
39
|
+
1. Config holds Nest Account `access_token` from `https://home.nest.com/session`.
|
|
40
|
+
2. Plugin calls `GET /session` with `Authorization: Basic <token>` and the web app's `cztoken` cookie.
|
|
41
|
+
3. Response yields session `access_token`, `userid`, and `urls.transport_url`.
|
|
42
|
+
4. REST calls use Basic auth with the session token; Observe uses the same token against `grpc-web.production.nest.com`.
|
|
43
|
+
|
|
44
|
+
## REST
|
|
45
|
+
|
|
46
|
+
- `POST /api/0.1/user/{userid}/app_launch` with bucket types including `topaz`, `structure`, `kryptonite`, `device`, `shared`, `where`, …
|
|
47
|
+
- Long-poll: `POST {transport_url}/v5/subscribe` with `{ objects: [{ object_key, object_revision, object_timestamp }] }`, client timeout ~120s. An idle abort is normal, not a failure.
|
|
48
|
+
|
|
49
|
+
## Observe
|
|
50
|
+
|
|
51
|
+
- Host: `grpc-web.production.nest.com`, path from session endpoints (`GatewayService/Observe`).
|
|
52
|
+
- Request body: opaque traits blob vendored from the probe kit (`assets/protobuf/ObserveTraits.protobuf`).
|
|
53
|
+
- Framing: length-delimited `nest.rpc.StreamBody`. Decode the full framed buffer; do not strip a header and assume success.
|
|
54
|
+
- Frame 0 is often a resource/trait catalog that fails NestMessage decode — ignore it.
|
|
55
|
+
- Later frames are trait snapshots/patches. Nest sends deltas; merge patches into state.
|
|
56
|
+
|
|
57
|
+
## Protect occupancy
|
|
58
|
+
|
|
59
|
+
A Protect has a PIR sensor, but neither API exposes usable motion events in practice. A ~12.5-hour capture of both transports on an occupied house recorded zero REST `auto_away` flips and zero nonempty Observe `ambient_motion` deltas. Many false "MOTION" banners in naive parsers were reconnect baselines containing the schema string `AmbientMotionEvent`, not live PIR.
|
|
60
|
+
|
|
61
|
+
What Nest does publish is `auto_away` on mains-powered Protects in REST `topaz`: roughly a ten-minute presence hold-off (`auto_away_decision_time_secs` is the window constant, not a timestamp). HomeKit occupancy is therefore:
|
|
62
|
+
|
|
63
|
+
- Published only when REST `auto_away` exists **and** the unit is confirmed mains-powered (`line_power_present` / Observe `wall_power`).
|
|
64
|
+
- Unavailable when power is unknown, for battery Protects, and for Observe-only Protects until a real event path is proven.
|
|
65
|
+
- Documented as presence, not pathlight-speed motion.
|
|
66
|
+
|
|
67
|
+
## Protect smoke / CO
|
|
68
|
+
|
|
69
|
+
Alarm status is taken from REST `topaz` (`smoke_status` / `co_status`), and only while the REST feed can refresh it. Observe streams `safety_alarm_*` traits, but no public schema maps them and captured samples have all been all-clear. Inferring "no smoke" from an unverified enum is refused.
|
|
70
|
+
|
|
71
|
+
An Observe-only Protect gets no smoke/CO HomeKit services until REST reports alarm state. If REST later becomes unavailable (circuit breaker open, HTTP 403 budget exhausted, or no successful subscribe/`app_launch` within the stale window), smoke/CO services stay published with last-known values but `StatusActive=false` and `StatusFault=GENERAL_FAULT` — leaving a frozen clear that still looks live would be dishonest, and tearing services down would break HomeKit rooms and automations on every Nest blip. Battery / online may still appear from Observe.
|
|
72
|
+
|
|
73
|
+
## Thermostat writes
|
|
74
|
+
|
|
75
|
+
HVAC state updates arrive over Observe (`target_temperature_settings` / `hvac_control` / `eco_mode_state`). Writes use the same protobuf gateway as the Nest web app:
|
|
76
|
+
|
|
77
|
+
### Mode / setpoints
|
|
78
|
+
|
|
79
|
+
1. Encode `nest.trait.hvac.TargetTemperatureSettingsTrait` (mode via `settings.hvacMode` + `active`; setpoints as `targetTemperatureHeat` / `targetTemperatureCool`).
|
|
80
|
+
2. Wrap in `nest.rpc.NestMessage` `{ set: [{ object: { id: DEVICE_…, key: target_temperature_settings, uuid }, property: Any }] }`.
|
|
81
|
+
3. `POST https://grpc-web.production.nest.com/nestlabs.gateway.v1.TraitBatchApi/BatchUpdateState` with `Content-Type: application/x-protobuf` and the session Basic token.
|
|
82
|
+
|
|
83
|
+
`off` is `active=0` with a standby `HEAT`/`COOL` left in `hvacMode` — Nest does not store an OFF enum there. REST `/v5/put` cannot reach Observe-only thermostats.
|
|
84
|
+
|
|
85
|
+
Plugin flag: `allowThermostatControl` (default **off**). Manual HomeKit setpoint/mode writes clear Nest Eco (`eco_mode_state` OFF) in the same batch when Eco was active.
|
|
86
|
+
|
|
87
|
+
> The encode shape was confirmed against a live account with a maintainer-only probe kit (`nest-probe`), which is **not part of this repository** and is not something a user or contributor can run. Treat references to it as provenance for where these findings came from, not as a step to reproduce.
|
|
88
|
+
|
|
89
|
+
### Eco Mode (on / off)
|
|
90
|
+
|
|
91
|
+
HomeKit has no Eco thermostat mode, so the plugin exposes Eco as a Switch (per thermostat, plus optional house-wide `exposeGlobalEcoSwitch`). Writes are a separate BatchUpdateState body:
|
|
92
|
+
|
|
93
|
+
1. Encode `nest.trait.hvac.EcoModeStateTrait` with `ecoEnabled` `ON` or `OFF` and `ecoModeChangeReason` `ECO_MODE_CHANGE_REASON_MANUAL`.
|
|
94
|
+
2. Wrap in `nest.rpc.NestMessage` `{ set: [{ object: { id: DEVICE_…, key: eco_mode_state, uuid }, property: Any }] }` (`encodeEcoModeBatchUpdate`).
|
|
95
|
+
3. Same `TraitBatchApi/BatchUpdateState` endpoint and auth as setpoint writes.
|
|
96
|
+
|
|
97
|
+
Eco-only writes were never confirmed against a live account the way setpoints were; enable `allowThermostatControl` only if you are comfortable with live Nest writes on your account.
|
|
98
|
+
|
|
99
|
+
The optional house-wide switch (`exposeGlobalEcoSwitch`) posts Eco to every live thermostat accessory. It reports success only when every write succeeds; HomeKit stays optimistic until Nest's all-Eco aggregate matches (or 45s elapses and Nest truth wins).
|
|
100
|
+
|
|
101
|
+
## Homebridge 2 update path
|
|
102
|
+
|
|
103
|
+
Never push live values with removed `Characteristic.getValue()`, and never call `updateValue(characteristic.value)` (that is the cached value). Store the getter next to the characteristic and call `updateValue(getFunc())` on Nest updates. See `src/utils/bound-characteristics.ts`.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# homebridge-mynest detailed documentation
|
|
2
|
+
|
|
3
|
+
Install and a short options table live in the [README](../README.md). This page is the rest: every option, what HomeKit shows when Nest is quiet, and how to read a failure.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Full configuration reference](#full-configuration-reference)
|
|
8
|
+
- [How devices appear](#how-devices-appear)
|
|
9
|
+
- [Protect occupancy](#protect-occupancy)
|
|
10
|
+
- [Smoke and CO honesty](#smoke-and-co-honesty)
|
|
11
|
+
- [How it works](#how-it-works)
|
|
12
|
+
- [Diagnostics](#diagnostics)
|
|
13
|
+
- [Troubleshooting](#troubleshooting)
|
|
14
|
+
- [Related docs](#related-docs)
|
|
15
|
+
|
|
16
|
+
## Full configuration reference
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"platforms": [
|
|
21
|
+
{
|
|
22
|
+
"platform": "MyNest",
|
|
23
|
+
"name": "MyNest",
|
|
24
|
+
"accessToken": "paste-access_token-from-home.nest.com/session",
|
|
25
|
+
"allowThermostatControl": false,
|
|
26
|
+
"exposeGlobalEcoSwitch": false,
|
|
27
|
+
"exposeProtectOccupancy": true,
|
|
28
|
+
"exposeProtectTemperature": false,
|
|
29
|
+
"ignoredDeviceIds": [],
|
|
30
|
+
"fieldTest": false,
|
|
31
|
+
"diagnosticsInterval": 0,
|
|
32
|
+
"structuredLogs": false,
|
|
33
|
+
"debug": false
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
| Option | Default | Description |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| `name` | `MyNest` | Required by Homebridge. Log prefix for this instance. |
|
|
42
|
+
| `accessToken` | — | Required. Nest Account `access_token` from [AUTH.md](AUTH.md). Google JWT and `ya29.` shapes are rejected. |
|
|
43
|
+
| `allowThermostatControl` | `false` | Opt in to send mode, setpoint, and Eco changes through Nest BatchUpdateState. With this off, HomeKit can still move the tile (Home needs that) but Nest ignores the write and the plugin reverts. |
|
|
44
|
+
| `exposeGlobalEcoSwitch` | `false` | Publish a Nest Eco Mode switch that sets Eco on every thermostat. Writes still require `allowThermostatControl`. |
|
|
45
|
+
| `exposeProtectOccupancy` | `true` | Occupancy from REST `auto_away` when Nest computes it on a mains-powered Protect. See [Protect occupancy](#protect-occupancy). |
|
|
46
|
+
| `exposeProtectTemperature` | `false` | Temperature and humidity measured by each Protect. |
|
|
47
|
+
| `ignoredDeviceIds` | `[]` | Device IDs or serials to leave out of HomeKit. |
|
|
48
|
+
| `fieldTest` | `false` | Use Nest field-test hosts. |
|
|
49
|
+
| `diagnosticsInterval` | `0` | Seconds between health heartbeats in the log. `0` is off. Otherwise `30`–`86400` (24h). |
|
|
50
|
+
| `structuredLogs` | `false` | When diagnostics are on, also emit a machine-readable JSON line next to each human summary. |
|
|
51
|
+
| `debug` | `false` | Verbose logging. Tokens are redacted. |
|
|
52
|
+
|
|
53
|
+
`name` is required by Homebridge verified plugins. The plugin uses it as the log prefix.
|
|
54
|
+
|
|
55
|
+
## How devices appear
|
|
56
|
+
|
|
57
|
+
| Nest device | HomeKit accessory | Notes |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| Thermostat | Thermostat + Eco Mode switch | Observe is source of truth. Modern thermostats may be missing from REST entirely. Mode, setpoints, and Eco are writable only when `allowThermostatControl` is on. Optional house-wide Eco switch via `exposeGlobalEcoSwitch`. |
|
|
60
|
+
| Nest Protect | Smoke + CO (+ optional occupancy / temp) | Smoke and CO need REST `topaz`. Battery and online can come from REST and/or Observe. |
|
|
61
|
+
| Temperature Sensor | Temperature Sensor | Battery + temperature (`kryptonite`). |
|
|
62
|
+
|
|
63
|
+
Cameras, doorbells, Yale locks, Home/Away structure switches, and Google-account-only homes are out of scope. That list is also in [SECURITY.md](../SECURITY.md).
|
|
64
|
+
|
|
65
|
+
A device leaves HomeKit only after Nest-confirmed drops (two-strike and truncation guards on Observe and REST). A cloud blip, a REST-only boot, or an expired access token stops updates but does not unregister thermostats, Protects, or temp sensors. Paste a fresh token and restart.
|
|
66
|
+
|
|
67
|
+
## Protect occupancy
|
|
68
|
+
|
|
69
|
+
Nest does not expose a reliable Protect motion event stream to third-party clients.
|
|
70
|
+
|
|
71
|
+
What this plugin publishes (when REST `auto_away` exists on a mains-powered Protect) is Nest's own presence hold-off: on the order of **ten minutes** after the room empties, clearing when activity is seen again. Do not build automations that expect pathlight-speed motion.
|
|
72
|
+
|
|
73
|
+
Occupancy is omitted, rather than shown as a stuck empty house, when:
|
|
74
|
+
|
|
75
|
+
- the Protect is battery-powered
|
|
76
|
+
- power is unknown
|
|
77
|
+
- the Protect is Observe-only (no REST `auto_away`)
|
|
78
|
+
- `exposeProtectOccupancy` is off
|
|
79
|
+
|
|
80
|
+
If REST later goes stale, a last occupancy reading stays published but is marked inactive/faulted.
|
|
81
|
+
|
|
82
|
+
## Smoke and CO honesty
|
|
83
|
+
|
|
84
|
+
Observe-only Protects omit smoke and CO until REST reports alarm state. The accessory still appears, so rooms and automations keep their targets.
|
|
85
|
+
|
|
86
|
+
If REST later goes down, those tiles stay in HomeKit but are marked inactive/faulted rather than freezing a live all-clear. Battery and online may remain from Observe.
|
|
87
|
+
|
|
88
|
+
The plugin does not invent an all-clear from unverified Observe fields.
|
|
89
|
+
|
|
90
|
+
## How it works
|
|
91
|
+
|
|
92
|
+
Two transports, neither a superset of the other:
|
|
93
|
+
|
|
94
|
+
| Path | Role |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| REST `app_launch` + `/v5/subscribe` | Protect alarm state (`topaz`), structure/where, temperature sensors, older thermostat buckets when present |
|
|
97
|
+
| Observe (HTTP/2) | Source of truth for modern thermostats; complete Protect inventory |
|
|
98
|
+
|
|
99
|
+
The plugin unions Observe ∪ REST and records which transport supplied each device. Independent circuit breakers fail fast when Nest's edge returns sustained 5xx or network failures, then probe again after a short cooldown. Auth and 403 paths keep their own handling.
|
|
100
|
+
|
|
101
|
+
Architecture and the Homebridge 2 update path: [DEVELOPMENT.md](../DEVELOPMENT.md). Wire-level notes: [PROTOCOL.md](PROTOCOL.md).
|
|
102
|
+
|
|
103
|
+
## Diagnostics
|
|
104
|
+
|
|
105
|
+
Set `diagnosticsInterval` to `30`–`86400` to log periodic health heartbeats and boot/shutdown snapshots. The heartbeat where health flips from healthy to degraded is a warning; later degraded heartbeats stay at info until it recovers. `0` (default) leaves diagnostics off.
|
|
106
|
+
|
|
107
|
+
`structuredLogs` adds a JSON line next to each human summary. Reports cover REST and Observe transport gauges, breaker state, device inventory, and API latency.
|
|
108
|
+
|
|
109
|
+
Nothing from diagnostics is exposed in HomeKit.
|
|
110
|
+
|
|
111
|
+
## Troubleshooting
|
|
112
|
+
|
|
113
|
+
1. **Authentication error.** Token missing, truncated, Google JWT/`ya29.`, or revoked. Capture a fresh Nest Account token ([AUTH.md](AUTH.md)).
|
|
114
|
+
2. **Thermostat missing.** Modern thermostats are Observe-only on some accounts. With default logging the plugin warns within about a minute if Observe produced no frames, and again every five minutes if a connected stream goes quiet. Set `debug: true` for stream detail. From a git checkout you can also run `npm run verify` against the live account. See [DEVELOPMENT.md](../DEVELOPMENT.md).
|
|
115
|
+
3. **Thermostat in Settings but no room tile.** Remove and re-add the **MyNest** child bridge in the Home app (or whatever you set as **Name**), then assign rooms again. Same-UUID republish does not clear Apple Home's stuck presentation.
|
|
116
|
+
4. **Eco / setpoint changes snap back.** Enable **Allow thermostat control**. With control off, HomeKit can still move the UI (required for tiles) but Nest ignores the write and the plugin reverts.
|
|
117
|
+
5. **Protect without smoke/CO.** Likely Observe-only (missing from REST). The accessory still appears; alarm tiles wait for REST.
|
|
118
|
+
6. **No occupancy.** Battery Protect, power unknown, Observe-only Protect, or `exposeProtectOccupancy` off. See [Protect occupancy](#protect-occupancy).
|
|
119
|
+
|
|
120
|
+
Restart Homebridge after any `config.json` edit.
|
|
121
|
+
|
|
122
|
+
## Related docs
|
|
123
|
+
|
|
124
|
+
- [README](../README.md): install and short options table
|
|
125
|
+
- [AUTH.md](AUTH.md): capturing the Nest Account token
|
|
126
|
+
- [PROTOCOL.md](PROTOCOL.md): reverse-engineered Nest behaviour
|
|
127
|
+
- [DEVELOPMENT.md](../DEVELOPMENT.md): architecture and local setup
|
|
128
|
+
- [SECURITY.md](../SECURITY.md)
|
|
129
|
+
- [CONTRIBUTING.md](../CONTRIBUTING.md)
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
Copyright 2026 tbaur
|
|
134
|
+
|
|
135
|
+
Licensed under the Apache License, Version 2.0. See [LICENSE](../LICENSE) file for details.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-mynest",
|
|
3
3
|
"displayName": "Homebridge My Nest",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.2",
|
|
5
5
|
"description": "Homebridge plugin for Nest thermostats (with Eco Mode), Nest Protect, and Nest Temperature Sensors using a Nest Account access token",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -30,12 +30,16 @@
|
|
|
30
30
|
},
|
|
31
31
|
"homepage": "https://github.com/tbaur/homebridge-mynest#readme",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"protobufjs": "7.6.
|
|
33
|
+
"protobufjs": "7.6.6"
|
|
34
|
+
},
|
|
35
|
+
"overrides": {
|
|
36
|
+
"js-yaml": "^3.15.2",
|
|
37
|
+
"@homebridge/hap-nodejs": "$@homebridge/hap-nodejs"
|
|
34
38
|
},
|
|
35
39
|
"devDependencies": {
|
|
36
40
|
"@homebridge/hap-nodejs": "^2.0.4",
|
|
37
41
|
"@types/jest": "^30.0.0",
|
|
38
|
-
"@types/node": "^
|
|
42
|
+
"@types/node": "^26.2.0",
|
|
39
43
|
"@typescript-eslint/eslint-plugin": "^8.51.0",
|
|
40
44
|
"@typescript-eslint/parser": "^8.51.0",
|
|
41
45
|
"eslint": "^10.7.0",
|
|
@@ -43,12 +47,12 @@
|
|
|
43
47
|
"homebridge": "^2.1.0",
|
|
44
48
|
"jest": "^30.4.2",
|
|
45
49
|
"nock": "^14.0.16",
|
|
46
|
-
"ts-jest": "29.4.
|
|
50
|
+
"ts-jest": "29.4.14",
|
|
47
51
|
"typescript": "6.0.3"
|
|
48
52
|
},
|
|
49
53
|
"engines": {
|
|
50
54
|
"homebridge": "^2.0.0",
|
|
51
|
-
"node": "^22 || ^24"
|
|
55
|
+
"node": "^22 || ^24 || ^26"
|
|
52
56
|
},
|
|
53
57
|
"peerDependencies": {
|
|
54
58
|
"homebridge": "^2.0.0"
|
|
@@ -80,6 +84,8 @@
|
|
|
80
84
|
"assets/**/*",
|
|
81
85
|
"config.schema.json",
|
|
82
86
|
"CHANGELOG.md",
|
|
83
|
-
"SECURITY.md"
|
|
87
|
+
"SECURITY.md",
|
|
88
|
+
"README.md",
|
|
89
|
+
"docs/**/*"
|
|
84
90
|
]
|
|
85
91
|
}
|