homebridge-mynest 1.0.0 → 2.0.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.
Files changed (160) hide show
  1. package/CHANGELOG.md +18 -2
  2. package/README.md +24 -23
  3. package/SECURITY.md +10 -6
  4. package/config.schema.json +16 -4
  5. package/dist/accessories/base.d.ts +8 -2
  6. package/dist/accessories/base.js +41 -5
  7. package/dist/accessories/global-eco.d.ts +8 -1
  8. package/dist/accessories/global-eco.js +53 -14
  9. package/dist/accessories/protect.d.ts +0 -1
  10. package/dist/accessories/protect.js +52 -27
  11. package/dist/accessories/temperature-sensor.d.ts +0 -1
  12. package/dist/accessories/temperature-sensor.js +1 -8
  13. package/dist/accessories/thermostat.d.ts +0 -1
  14. package/dist/accessories/thermostat.js +20 -17
  15. package/dist/api/batch-update.d.ts +0 -1
  16. package/dist/api/batch-update.js +7 -4
  17. package/dist/api/circuit-breaker.d.ts +0 -1
  18. package/dist/api/circuit-breaker.js +0 -1
  19. package/dist/api/framing.d.ts +0 -1
  20. package/dist/api/framing.js +84 -5
  21. package/dist/api/http.d.ts +2 -3
  22. package/dist/api/http.js +86 -6
  23. package/dist/api/observe.d.ts +1 -1
  24. package/dist/api/observe.js +86 -13
  25. package/dist/api/protobuf.d.ts +17 -2
  26. package/dist/api/protobuf.js +74 -26
  27. package/dist/api/rest.d.ts +25 -4
  28. package/dist/api/rest.js +78 -13
  29. package/dist/api/session.d.ts +0 -1
  30. package/dist/api/session.js +34 -11
  31. package/dist/api/thermostat-write.d.ts +10 -5
  32. package/dist/api/thermostat-write.js +47 -14
  33. package/dist/api/transport.d.ts +18 -1
  34. package/dist/api/transport.js +367 -44
  35. package/dist/diagnostics/collector.d.ts +0 -1
  36. package/dist/diagnostics/collector.js +6 -1
  37. package/dist/diagnostics/format.d.ts +0 -1
  38. package/dist/diagnostics/format.js +1 -4
  39. package/dist/diagnostics/types.d.ts +3 -2
  40. package/dist/diagnostics/types.js +0 -1
  41. package/dist/errors/index.d.ts +2 -3
  42. package/dist/errors/index.js +21 -14
  43. package/dist/index.d.ts +0 -1
  44. package/dist/index.js +0 -1
  45. package/dist/platform.d.ts +0 -1
  46. package/dist/platform.js +250 -50
  47. package/dist/settings.d.ts +195 -7
  48. package/dist/settings.js +203 -8
  49. package/dist/state/classify.d.ts +0 -1
  50. package/dist/state/classify.js +20 -3
  51. package/dist/state/observe-state.d.ts +17 -1
  52. package/dist/state/observe-state.js +53 -7
  53. package/dist/state/protect-state.d.ts +10 -5
  54. package/dist/state/protect-state.js +10 -5
  55. package/dist/state/registry.d.ts +0 -1
  56. package/dist/state/registry.js +43 -13
  57. package/dist/state/sensor-state.d.ts +0 -1
  58. package/dist/state/sensor-state.js +7 -2
  59. package/dist/state/thermostat-state.d.ts +0 -1
  60. package/dist/state/thermostat-state.js +20 -13
  61. package/dist/state/traits.d.ts +0 -10
  62. package/dist/state/traits.js +5 -2
  63. package/dist/types/config.d.ts +8 -2
  64. package/dist/types/config.js +0 -1
  65. package/dist/types/device.d.ts +0 -1
  66. package/dist/types/device.js +0 -1
  67. package/dist/types/nest.d.ts +33 -4
  68. package/dist/types/nest.js +0 -1
  69. package/dist/utils/bound-characteristics.d.ts +7 -2
  70. package/dist/utils/bound-characteristics.js +31 -3
  71. package/dist/utils/logger.d.ts +0 -1
  72. package/dist/utils/logger.js +0 -1
  73. package/dist/utils/retry.d.ts +13 -3
  74. package/dist/utils/retry.js +32 -6
  75. package/dist/utils/sanitizers.d.ts +0 -1
  76. package/dist/utils/sanitizers.js +23 -3
  77. package/dist/utils/validators.d.ts +0 -1
  78. package/dist/utils/validators.js +5 -10
  79. package/package.json +20 -30
  80. package/.env.example +0 -20
  81. package/CONTRIBUTING.md +0 -40
  82. package/DEVELOPMENT.md +0 -73
  83. package/RELEASING.md +0 -55
  84. package/dist/accessories/base.d.ts.map +0 -1
  85. package/dist/accessories/base.js.map +0 -1
  86. package/dist/accessories/global-eco.d.ts.map +0 -1
  87. package/dist/accessories/global-eco.js.map +0 -1
  88. package/dist/accessories/protect.d.ts.map +0 -1
  89. package/dist/accessories/protect.js.map +0 -1
  90. package/dist/accessories/temperature-sensor.d.ts.map +0 -1
  91. package/dist/accessories/temperature-sensor.js.map +0 -1
  92. package/dist/accessories/thermostat.d.ts.map +0 -1
  93. package/dist/accessories/thermostat.js.map +0 -1
  94. package/dist/api/batch-update.d.ts.map +0 -1
  95. package/dist/api/batch-update.js.map +0 -1
  96. package/dist/api/circuit-breaker.d.ts.map +0 -1
  97. package/dist/api/circuit-breaker.js.map +0 -1
  98. package/dist/api/framing.d.ts.map +0 -1
  99. package/dist/api/framing.js.map +0 -1
  100. package/dist/api/http.d.ts.map +0 -1
  101. package/dist/api/http.js.map +0 -1
  102. package/dist/api/observe.d.ts.map +0 -1
  103. package/dist/api/observe.js.map +0 -1
  104. package/dist/api/protobuf.d.ts.map +0 -1
  105. package/dist/api/protobuf.js.map +0 -1
  106. package/dist/api/rest.d.ts.map +0 -1
  107. package/dist/api/rest.js.map +0 -1
  108. package/dist/api/session.d.ts.map +0 -1
  109. package/dist/api/session.js.map +0 -1
  110. package/dist/api/thermostat-write.d.ts.map +0 -1
  111. package/dist/api/thermostat-write.js.map +0 -1
  112. package/dist/api/transport.d.ts.map +0 -1
  113. package/dist/api/transport.js.map +0 -1
  114. package/dist/diagnostics/collector.d.ts.map +0 -1
  115. package/dist/diagnostics/collector.js.map +0 -1
  116. package/dist/diagnostics/format.d.ts.map +0 -1
  117. package/dist/diagnostics/format.js.map +0 -1
  118. package/dist/diagnostics/types.d.ts.map +0 -1
  119. package/dist/diagnostics/types.js.map +0 -1
  120. package/dist/errors/index.d.ts.map +0 -1
  121. package/dist/errors/index.js.map +0 -1
  122. package/dist/index.d.ts.map +0 -1
  123. package/dist/index.js.map +0 -1
  124. package/dist/platform.d.ts.map +0 -1
  125. package/dist/platform.js.map +0 -1
  126. package/dist/settings.d.ts.map +0 -1
  127. package/dist/settings.js.map +0 -1
  128. package/dist/state/classify.d.ts.map +0 -1
  129. package/dist/state/classify.js.map +0 -1
  130. package/dist/state/observe-state.d.ts.map +0 -1
  131. package/dist/state/observe-state.js.map +0 -1
  132. package/dist/state/protect-state.d.ts.map +0 -1
  133. package/dist/state/protect-state.js.map +0 -1
  134. package/dist/state/registry.d.ts.map +0 -1
  135. package/dist/state/registry.js.map +0 -1
  136. package/dist/state/sensor-state.d.ts.map +0 -1
  137. package/dist/state/sensor-state.js.map +0 -1
  138. package/dist/state/thermostat-state.d.ts.map +0 -1
  139. package/dist/state/thermostat-state.js.map +0 -1
  140. package/dist/state/traits.d.ts.map +0 -1
  141. package/dist/state/traits.js.map +0 -1
  142. package/dist/types/config.d.ts.map +0 -1
  143. package/dist/types/config.js.map +0 -1
  144. package/dist/types/device.d.ts.map +0 -1
  145. package/dist/types/device.js.map +0 -1
  146. package/dist/types/nest.d.ts.map +0 -1
  147. package/dist/types/nest.js.map +0 -1
  148. package/dist/utils/bound-characteristics.d.ts.map +0 -1
  149. package/dist/utils/bound-characteristics.js.map +0 -1
  150. package/dist/utils/logger.d.ts.map +0 -1
  151. package/dist/utils/logger.js.map +0 -1
  152. package/dist/utils/retry.d.ts.map +0 -1
  153. package/dist/utils/retry.js.map +0 -1
  154. package/dist/utils/sanitizers.d.ts.map +0 -1
  155. package/dist/utils/sanitizers.js.map +0 -1
  156. package/dist/utils/validators.d.ts.map +0 -1
  157. package/dist/utils/validators.js.map +0 -1
  158. package/docs/AUTH.md +0 -26
  159. package/docs/PROTOCOL.md +0 -97
  160. package/scripts/verify.mjs +0 -222
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
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.0](https://github.com/tbaur/homebridge-mynest/compare/v1.1.0...v2.0.0) (2026-08-05)
6
+
7
+
8
+ ### ⚠ BREAKING CHANGES
9
+
10
+ * Homebridge 1.x is no longer supported. Requires Homebridge 2.x and Node.js 22 or newer, which is what Homebridge 2 itself requires.
11
+
12
+ ### Features
13
+
14
+ * harden for Homebridge 2 — breaking release to 2.0.0 ([#29](https://github.com/tbaur/homebridge-mynest/issues/29)) ([02a2b7d](https://github.com/tbaur/homebridge-mynest/commit/02a2b7de0da39db36a4884f010534ab9a095daa5))
15
+
16
+ ## [1.1.0](https://github.com/tbaur/homebridge-mynest/compare/v1.0.0...v1.1.0) (2026-08-01)
17
+
18
+
19
+ ### Features
20
+
21
+ * allow diagnostics interval up to 24h as a number field ([#27](https://github.com/tbaur/homebridge-mynest/issues/27)) ([9f988f7](https://github.com/tbaur/homebridge-mynest/commit/9f988f7d2cc21c5e6e072ce1914b0d767aa0168e))
22
+
5
23
  ## [1.0.0](https://github.com/tbaur/homebridge-mynest/compare/v0.1.10...v1.0.0) (2026-07-31)
6
24
 
7
25
 
@@ -79,8 +97,6 @@ All notable changes to this project are documented here. From the first release
79
97
 
80
98
  * show Ignored Device IDs input in Homebridge UI ([#3](https://github.com/tbaur/homebridge-mynest/issues/3)) ([0e7d966](https://github.com/tbaur/homebridge-mynest/commit/0e7d9668ae1e9dc01da5fd5e04c8aceeae361fe6))
81
99
 
82
- ## [Unreleased]
83
-
84
100
  ## 0.1.0
85
101
 
86
102
  Initial public release of **homebridge-mynest**.
package/README.md CHANGED
@@ -3,8 +3,8 @@
3
3
  [![Tests](https://github.com/tbaur/homebridge-mynest/actions/workflows/test.yml/badge.svg)](https://github.com/tbaur/homebridge-mynest/actions/workflows/test.yml)
4
4
  [![npm version](https://img.shields.io/npm/v/homebridge-mynest?style=flat-square)](https://www.npmjs.com/package/homebridge-mynest)
5
5
  [![npm downloads](https://img.shields.io/npm/dt/homebridge-mynest?style=flat-square)](https://www.npmjs.com/package/homebridge-mynest)
6
- [![Node.js](https://img.shields.io/badge/node-%3E%3D20-green)](https://nodejs.org)
7
- [![Homebridge](https://img.shields.io/badge/homebridge-%3E%3D1.6.0%20%7C%7C%202.x-purple)](https://homebridge.io)
6
+ [![Node.js](https://img.shields.io/badge/node-22%20%7C%7C%2024-green)](https://nodejs.org)
7
+ [![Homebridge](https://img.shields.io/badge/homebridge-2.x-purple)](https://homebridge.io)
8
8
  [![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
9
9
 
10
10
  Expose Nest thermostats, Nest Protect smoke/CO alarms, and Nest Temperature Sensors in Apple HomeKit through Homebridge, using a **Nest Account** access token only.
@@ -33,10 +33,10 @@ Expose Nest thermostats, Nest Protect smoke/CO alarms, and Nest Temperature Sens
33
33
  ### Quality
34
34
 
35
35
  <!-- Canonical test count lives here only; keep other docs number-free to avoid multi-place updates. -->
36
- - **Homebridge 2 safe** — Live updates use stored getters + `updateValue(...)`, not removed `getValue()` or stale `.value` reads
37
- - **452 Tests** — Jest suite with an 80% coverage gate across statements, branches, functions, and lines
38
- - **Strict TypeScript** — `strict` mode with unused locals/params and no implicit returns
39
- - **Secret hygiene** — Access tokens are redacted from logs
36
+ - **Built for Homebridge 2** — Live updates use stored getters + `updateValue(...)`, not the removed `getValue()` or stale `.value` reads
37
+ - **576 tests** — Jest suite with a per-area coverage floor on every source directory (statements, branches, functions, and lines), and a CI step that fails if the suite leaks an open handle
38
+ - **Strict TypeScript** — `strict` mode with unused locals/params and no implicit returns, plus type-aware linting (no floating promises)
39
+ - **Secret hygiene** — Access tokens are redacted from logs, and untrusted Nest responses cannot reach `Object.prototype`
40
40
  - **No analytics** — Zero tracking or data collection
41
41
 
42
42
  ## Quick Start
@@ -53,7 +53,7 @@ npm install -g homebridge-mynest
53
53
 
54
54
  ### 2. Get a Nest Account token
55
55
 
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](docs/AUTH.md).
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](https://github.com/tbaur/homebridge-mynest/blob/main/docs/AUTH.md).
57
57
 
58
58
  Prefer treating that token like a password; Nest Account sessions are account-scoped credentials.
59
59
 
@@ -75,7 +75,7 @@ Use the Homebridge UI, or add the platform to `config.json`:
75
75
 
76
76
  ### 4. Restart Homebridge
77
77
 
78
- Thermostats, Protects, and temperature sensors appear in the Home app as Nest reports them. The log should show `Connected to Nest`, then device adds, then `Platform ready`.
78
+ 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
79
 
80
80
  Thermostat control (mode, setpoints, Eco) stays off until you enable **Allow thermostat control**. Optionally enable **Expose global Eco switch** for a house-wide Nest Eco Mode tile.
81
81
 
@@ -87,7 +87,7 @@ Thermostat control (mode, setpoints, Eco) stays off until you enable **Allow the
87
87
  | Nest Protect | Smoke + CO (+ optional occupancy / temp) | Smoke/CO require REST `topaz`; occupancy is ~10-minute presence |
88
88
  | Temperature Sensor | Temperature Sensor | Battery + temperature |
89
89
 
90
- Cameras, doorbells, locks, and Home/Away structure switches are out of scope.
90
+ Cameras, doorbells, Yale locks, Home/Away structure switches, and Google-account-only homes are out of scope.
91
91
 
92
92
  ## Configuration Options
93
93
 
@@ -96,14 +96,14 @@ Cameras, doorbells, locks, and Home/Away structure switches are out of scope.
96
96
  | Option | Default | Description |
97
97
  | --- | --- | --- |
98
98
  | `name` | `MyNest` | Required. Plugin instance name shown in Homebridge logs. |
99
- | `accessToken` | — | Required. Nest Account `access_token` from [docs/AUTH.md](docs/AUTH.md). |
99
+ | `accessToken` | — | Required. Nest Account `access_token` from [docs/AUTH.md](https://github.com/tbaur/homebridge-mynest/blob/main/docs/AUTH.md). |
100
100
  | `allowThermostatControl` | `false` | Opt in to send mode/setpoint/Eco changes to Nest via BatchUpdateState. |
101
101
  | `exposeGlobalEcoSwitch` | `false` | Publish a Nest Eco Mode switch that sets Eco on every thermostat. Writes require `allowThermostatControl`. |
102
102
  | `exposeProtectOccupancy` | `true` | Occupancy from REST `auto_away` when Nest computes it on a mains-powered Protect. |
103
103
  | `exposeProtectTemperature` | `false` | Temperature/humidity measured by each Protect. |
104
104
  | `ignoredDeviceIds` | `[]` | Device IDs or serials to leave out of HomeKit. |
105
105
  | `fieldTest` | `false` | Use Nest field-test hosts. |
106
- | `diagnosticsInterval` | `0` | Seconds between health heartbeats in the log; `0` off, else `30`–`3600`. |
106
+ | `diagnosticsInterval` | `0` | Seconds between health heartbeats in the log; `0` off, else `30`–`86400` (24h). |
107
107
  | `structuredLogs` | `false` | When diagnostics are enabled, also emit a machine-readable JSON line alongside each human summary. |
108
108
  | `debug` | `false` | Verbose logging; tokens are redacted. |
109
109
 
@@ -113,9 +113,9 @@ Nest does not expose a reliable Protect motion event stream to third-party clien
113
113
 
114
114
  ## Not Working?
115
115
 
116
- 1. **Authentication error** — Token missing, truncated, Google JWT/`ya29.`, or revoked. Capture a fresh Nest Account token ([docs/AUTH.md](docs/AUTH.md)).
117
- 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; set `debug: true` for stream detail, or run `npm run verify`.
118
- 3. **Thermostat in Settings but no room tile** — Remove and re-add the My Nest child bridge in the Home app, then assign rooms again. Same-UUID republish does not clear Apple Home's stuck presentation.
116
+ 1. **Authentication error** — Token missing, truncated, Google JWT/`ya29.`, or revoked. Capture a fresh Nest Account token ([docs/AUTH.md](https://github.com/tbaur/homebridge-mynest/blob/main/docs/AUTH.md)).
117
+ 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](https://github.com/tbaur/homebridge-mynest/blob/main/DEVELOPMENT.md).
118
+ 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.
119
119
  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.
120
120
  5. **Protect without smoke/CO** — Likely Observe-only (missing from REST). The accessory still appears; alarm tiles wait for REST.
121
121
  6. **No occupancy** — Battery Protect, power unknown, Observe-only Protect, or `exposeProtectOccupancy` off.
@@ -124,24 +124,25 @@ Nest does not expose a reliable Protect motion event stream to third-party clien
124
124
 
125
125
  This plugin holds a Nest Account `access_token` in Homebridge's plaintext `config.json`. That token is account-scoped Nest credentials — anyone who can read the file can act as the Nest web app for the home. Secure the host, prefer rotating the token if it may have leaked, and never paste tokens into issues or logs.
126
126
 
127
- Details: [SECURITY.md](SECURITY.md) and [docs/AUTH.md](docs/AUTH.md).
127
+ Details: [SECURITY.md](https://github.com/tbaur/homebridge-mynest/blob/main/SECURITY.md) and [docs/AUTH.md](https://github.com/tbaur/homebridge-mynest/blob/main/docs/AUTH.md).
128
128
 
129
129
  Nest publishes no consumer API and can change or revoke sessions without notice. This plugin uses Nest Account tokens only and rejects Google cookie / `ya29.` shapes on purpose.
130
130
 
131
131
  ## Requirements
132
132
 
133
- - Node.js 20 or newer
134
- - Homebridge 1.6 or newer, including Homebridge 2.x
133
+ - **Homebridge 2.x** — Homebridge 1.x is not supported
134
+ - **Node.js 22 or 24** — this mirrors Homebridge 2's own `engines` constraint, so newer Node versions are excluded until Homebridge supports them; CI tests both
135
135
  - A Nest Account (not Google-only) with an `access_token` from [home.nest.com/session](https://home.nest.com/session)
136
136
 
137
137
  ## More Info
138
138
 
139
- - [Authentication](docs/AUTH.md) — capturing the Nest Account token
140
- - [Protocol notes](docs/PROTOCOL.md) — reverse-engineered Nest behaviour
141
- - [Development](DEVELOPMENT.md) — architecture and local setup
142
- - [Contributing](CONTRIBUTING.md)
143
- - [Security policy](SECURITY.md)
144
- - [Changelog](CHANGELOG.md)
139
+ - [Authentication](https://github.com/tbaur/homebridge-mynest/blob/main/docs/AUTH.md) — capturing the Nest Account token
140
+ - [Protocol notes](https://github.com/tbaur/homebridge-mynest/blob/main/docs/PROTOCOL.md) — reverse-engineered Nest behaviour
141
+ - [Development](https://github.com/tbaur/homebridge-mynest/blob/main/DEVELOPMENT.md) — architecture and local setup
142
+ - [Contributing](https://github.com/tbaur/homebridge-mynest/blob/main/CONTRIBUTING.md)
143
+ - [Code of conduct](https://github.com/tbaur/homebridge-mynest/blob/main/CODE_OF_CONDUCT.md)
144
+ - [Security policy](https://github.com/tbaur/homebridge-mynest/blob/main/SECURITY.md)
145
+ - [Changelog](https://github.com/tbaur/homebridge-mynest/blob/main/CHANGELOG.md)
145
146
  - [Report issues](https://github.com/tbaur/homebridge-mynest/issues)
146
147
 
147
148
  ## License
package/SECURITY.md CHANGED
@@ -2,19 +2,21 @@
2
2
 
3
3
  ## Supported Versions
4
4
 
5
- | Version | Supported |
5
+ Only the latest published release is supported. Fixes are shipped forward rather than backported, so the answer to "is my version supported?" is always "upgrade to the newest release".
6
+
7
+ | Version | Supported |
6
8
  | ------- | ----------------- |
7
- | 1.x.x | ✅ Active support |
8
- | 0.1.x | ❌ Unsupported (upgrade to 1.x) |
9
+ | Latest release | ✅ Active support |
10
+ | Anything older | ❌ Unsupported — upgrade |
9
11
 
10
- Only the latest 1.x release is supported. Fixes are shipped forward rather than backported.
12
+ The next release requires **Homebridge 2.x and Node.js 22 or newer**; Homebridge 1.x is no longer supported.
11
13
 
12
14
  ## Reporting a Vulnerability
13
15
 
14
16
  If you discover a security vulnerability, please report it responsibly:
15
17
 
16
18
  1. **Do NOT open a public issue**
17
- 2. Email the maintainer directly or use GitHub's [private vulnerability reporting](https://github.com/tbaur/homebridge-mynest/security/advisories/new)
19
+ 2. Use GitHub's [private vulnerability reporting](https://github.com/tbaur/homebridge-mynest/security/advisories/new)
18
20
  3. Include:
19
21
  - Description of the vulnerability
20
22
  - Steps to reproduce
@@ -47,4 +49,6 @@ This plugin implements:
47
49
 
48
50
  ## Scope limits
49
51
 
50
- Cameras, doorbells, Yale locks, and Google-account-only homes are out of scope. Life-safety alarm state for Protects is only published while Nest REST can refresh it; Observe-only units and Protects whose REST feed is down do not keep a frozen all-clear.
52
+ Out-of-scope devices are listed in the [README](README.md#supported-devices): cameras, doorbells, Yale locks, Home/Away structure switches, and Google-account-only homes.
53
+
54
+ Life-safety alarm state for Protects is only published while Nest REST can refresh it; Observe-only units and Protects whose REST feed is down do not keep a frozen all-clear.
@@ -28,7 +28,7 @@
28
28
  "title": "Allow thermostat control",
29
29
  "type": "boolean",
30
30
  "default": false,
31
- "description": "Opt in to let HomeKit change thermostat mode, setpoints, and Eco via Nest BatchUpdateState. Off by default so upgrades stay read-only until you enable control. Confirm setpoints with nest-probe probe 12 (--confirm) first; Eco Mode uses the same gateway but has no separate probe confirm yet."
31
+ "description": "Opt in to let HomeKit change thermostat mode, setpoints, and Eco via Nest BatchUpdateState. Off by default so upgrades stay read-only until you enable control. Nest publishes no supported write API, so leave this off unless you want HomeKit driving your HVAC."
32
32
  },
33
33
  "exposeGlobalEcoSwitch": {
34
34
  "title": "Expose global Eco switch",
@@ -51,6 +51,7 @@
51
51
  "ignoredDeviceIds": {
52
52
  "title": "Ignored device IDs",
53
53
  "type": "array",
54
+ "default": [],
54
55
  "required": false,
55
56
  "description": "Nest device IDs or serial numbers to leave out of HomeKit. IDs appear in the Homebridge log when devices are added.",
56
57
  "items": {
@@ -70,8 +71,11 @@
70
71
  "type": "integer",
71
72
  "default": 0,
72
73
  "minimum": 0,
73
- "maximum": 3600,
74
- "description": "Seconds between health heartbeats in the Homebridge log. 0 disables (default). Values 1–29 are raised to 30."
74
+ "maximum": 86400,
75
+ "x-schema-form": {
76
+ "type": "number"
77
+ },
78
+ "description": "Seconds between health heartbeats in the Homebridge log. 0 disables (default). Allowed range 0–86400 (24h); values 1–29 are raised to 30."
75
79
  },
76
80
  "structuredLogs": {
77
81
  "title": "Structured diagnostic logs",
@@ -116,7 +120,15 @@
116
120
  "title": "Advanced",
117
121
  "expandable": true,
118
122
  "expanded": false,
119
- "items": ["fieldTest", "diagnosticsInterval", "structuredLogs", "debug"]
123
+ "items": [
124
+ "fieldTest",
125
+ {
126
+ "key": "diagnosticsInterval",
127
+ "type": "number"
128
+ },
129
+ "structuredLogs",
130
+ "debug"
131
+ ]
120
132
  }
121
133
  ]
122
134
  }
@@ -12,7 +12,7 @@
12
12
  * working on Homebridge 2 — see `utils/bound-characteristics.ts` for why the
13
13
  * obvious alternatives silently stop updating.
14
14
  */
15
- import type { PlatformAccessory, Service } from 'homebridge';
15
+ import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge';
16
16
  import type { DeviceIdentity, DeviceKind, NestDevice } from '../types/device';
17
17
  import type { ResolvedConfig } from '../types/config';
18
18
  import type { Logger } from '../utils/logger';
@@ -78,5 +78,11 @@ export declare abstract class NestAccessory<TState> {
78
78
  * reporting whatever value it last held.
79
79
  */
80
80
  protected removeService(type: Parameters<PlatformAccessory['getService']>[0]): void;
81
+ /**
82
+ * Map a low-battery verdict onto HomeKit's enum.
83
+ *
84
+ * `undefined` when Nest has said nothing, so the binder leaves the last known
85
+ * value in place rather than publishing "normal" on no evidence.
86
+ */
87
+ protected toLowBatteryValue(isBatteryLow: boolean | undefined): CharacteristicValue | undefined;
81
88
  }
82
- //# sourceMappingURL=base.d.ts.map
@@ -17,6 +17,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.NestAccessory = void 0;
18
18
  const settings_1 = require("../settings");
19
19
  const bound_characteristics_1 = require("../utils/bound-characteristics");
20
+ /** Whether any field published as Accessory Information differs. */
21
+ function hasIdentityChanged(previous, next) {
22
+ return previous.name !== next.name
23
+ || previous.model !== next.model
24
+ || previous.serialNumber !== next.serialNumber
25
+ || previous.firmwareVersion !== next.firmwareVersion
26
+ || previous.id !== next.id;
27
+ }
20
28
  /** One HomeKit accessory backed by one Nest device. */
21
29
  class NestAccessory {
22
30
  platform;
@@ -35,7 +43,7 @@ class NestAccessory {
35
43
  this.config = platform.resolvedConfig;
36
44
  this.identity = device.identity;
37
45
  this.state = device.state;
38
- this.binder = new bound_characteristics_1.CharacteristicBinder(log);
46
+ this.binder = new bound_characteristics_1.CharacteristicBinder(log, device.identity.name);
39
47
  this.#applyAccessoryInformation();
40
48
  }
41
49
  get deviceId() {
@@ -49,9 +57,15 @@ class NestAccessory {
49
57
  * unconditionally is cheaper than diffing here.
50
58
  */
51
59
  update(device) {
60
+ const previousIdentity = this.identity;
52
61
  this.identity = device.identity;
53
62
  this.state = device.state;
54
- this.#applyAccessoryInformation();
63
+ // Only when it actually changed. These four values essentially never do, and
64
+ // re-applying them ran HAP's full value-validation path four times per
65
+ // device per refresh on the hot update path.
66
+ if (hasIdentityChanged(previousIdentity, device.identity)) {
67
+ this.#applyAccessoryInformation();
68
+ }
55
69
  this.onServicesMayChange();
56
70
  this.binder.refresh();
57
71
  this.#logSummary();
@@ -93,6 +107,21 @@ class NestAccessory {
93
107
  this.binder.unbindService(existing);
94
108
  this.accessory.removeService(existing);
95
109
  }
110
+ /**
111
+ * Map a low-battery verdict onto HomeKit's enum.
112
+ *
113
+ * `undefined` when Nest has said nothing, so the binder leaves the last known
114
+ * value in place rather than publishing "normal" on no evidence.
115
+ */
116
+ toLowBatteryValue(isBatteryLow) {
117
+ const { StatusLowBattery } = this.platform.Characteristic;
118
+ if (isBatteryLow === undefined) {
119
+ return undefined;
120
+ }
121
+ return isBatteryLow
122
+ ? StatusLowBattery.BATTERY_LEVEL_LOW
123
+ : StatusLowBattery.BATTERY_LEVEL_NORMAL;
124
+ }
96
125
  #applyAccessoryInformation() {
97
126
  const { Characteristic, Service: HapService } = this.platform;
98
127
  const service = this.accessory.getService(HapService.AccessoryInformation)
@@ -106,10 +135,18 @@ class NestAccessory {
106
135
  service.setCharacteristic(Characteristic.FirmwareRevision, this.identity.firmwareVersion);
107
136
  }
108
137
  }
109
- /** Info on a change, debug otherwise, so the log follows the house. */
138
+ /**
139
+ * Info on a change, debug otherwise, so the log follows the house.
140
+ *
141
+ * The first summary is also info: it is the only default-visible confirmation
142
+ * that a newly published device is actually reporting data. The "Added …"
143
+ * line carries no readings, so folding the first summary into the debug
144
+ * branch left an operator unable to tell a live device from a silent one.
145
+ */
110
146
  #logSummary() {
111
147
  const summary = this.describeState();
112
- if (this.#lastSummary === null || this.#lastSummary === summary) {
148
+ const isFirst = this.#lastSummary === null;
149
+ if (!isFirst && this.#lastSummary === summary) {
113
150
  this.log.debug(`${this.identity.name}: ${summary}`);
114
151
  }
115
152
  else {
@@ -119,4 +156,3 @@ class NestAccessory {
119
156
  }
120
157
  }
121
158
  exports.NestAccessory = NestAccessory;
122
- //# sourceMappingURL=base.js.map
@@ -24,5 +24,12 @@ export declare class GlobalEcoAccessory {
24
24
  * matches within {@link PENDING_ECO_MAX_MS}, take Nest truth.
25
25
  */
26
26
  updateAllEco(allEco: boolean): void;
27
+ /**
28
+ * Release the pending-write backstop.
29
+ *
30
+ * Called when the switch is removed or the platform stops. The timer is
31
+ * `unref`'d so it cannot hold the process open, but without this it could
32
+ * still fire up to 45s later and refresh a torn-down accessory.
33
+ */
34
+ dispose(): void;
27
35
  }
28
- //# sourceMappingURL=global-eco.d.ts.map
@@ -15,7 +15,6 @@ exports.GlobalEcoAccessory = void 0;
15
15
  const settings_1 = require("../settings");
16
16
  const bound_characteristics_1 = require("../utils/bound-characteristics");
17
17
  const sanitizers_1 = require("../utils/sanitizers");
18
- const DISPLAY_NAME = 'Nest Eco Mode';
19
18
  /**
20
19
  * How long a successful HomeKit Eco write may ignore Nest aggregates that have
21
20
  * not yet matched. After this, Nest truth wins (Nest app / failed Observe).
@@ -35,11 +34,19 @@ class GlobalEcoAccessory {
35
34
  */
36
35
  #pendingAllEco = null;
37
36
  #pendingSinceMs = null;
37
+ /**
38
+ * Backstop for {@link PENDING_ECO_MAX_MS}.
39
+ *
40
+ * The age check in {@link updateAllEco} only runs when Nest sends another
41
+ * update. If Observe dies right after a successful write, nothing would ever
42
+ * clear the optimistic value and the switch would hold it indefinitely.
43
+ */
44
+ #pendingTimer = null;
38
45
  constructor(platform, accessory, log) {
39
46
  this.#platform = platform;
40
47
  this.#accessory = accessory;
41
48
  this.#log = log;
42
- this.#binder = new bound_characteristics_1.CharacteristicBinder(log);
49
+ this.#binder = new bound_characteristics_1.CharacteristicBinder(log, settings_1.GLOBAL_ECO_DISPLAY_NAME);
43
50
  this.#applyAccessoryInformation();
44
51
  this.#bindSwitch();
45
52
  this.#binder.refresh();
@@ -58,8 +65,7 @@ class GlobalEcoAccessory {
58
65
  }
59
66
  else if (this.#pendingSinceMs !== null
60
67
  && Date.now() - this.#pendingSinceMs >= PENDING_ECO_MAX_MS) {
61
- this.#log.warn(`${DISPLAY_NAME}: Nest did not confirm Eco change within ${PENDING_ECO_MAX_MS / 1000}s — following Nest`);
62
- this.#clearPending();
68
+ this.#expirePending();
63
69
  }
64
70
  else {
65
71
  return;
@@ -68,15 +74,50 @@ class GlobalEcoAccessory {
68
74
  this.#allEco = allEco;
69
75
  this.#binder.refresh();
70
76
  }
77
+ /**
78
+ * Release the pending-write backstop.
79
+ *
80
+ * Called when the switch is removed or the platform stops. The timer is
81
+ * `unref`'d so it cannot hold the process open, but without this it could
82
+ * still fire up to 45s later and refresh a torn-down accessory.
83
+ */
84
+ dispose() {
85
+ this.#clearPending();
86
+ }
71
87
  #clearPending() {
72
88
  this.#pendingAllEco = null;
73
89
  this.#pendingSinceMs = null;
90
+ if (this.#pendingTimer) {
91
+ clearTimeout(this.#pendingTimer);
92
+ this.#pendingTimer = null;
93
+ }
94
+ }
95
+ /** Give up on an unconfirmed write and let Nest's own value stand. */
96
+ #expirePending() {
97
+ this.#log.warn(`${settings_1.GLOBAL_ECO_DISPLAY_NAME}: Nest did not confirm Eco change within ${PENDING_ECO_MAX_MS / 1000}s — following Nest`);
98
+ this.#clearPending();
99
+ }
100
+ /** Start the backstop that gives Nest the last word even if it goes quiet. */
101
+ #armPending(ecoOn) {
102
+ this.#clearPending();
103
+ this.#pendingAllEco = ecoOn;
104
+ this.#pendingSinceMs = Date.now();
105
+ this.#pendingTimer = setTimeout(() => {
106
+ this.#pendingTimer = null;
107
+ if (this.#pendingAllEco === null) {
108
+ return;
109
+ }
110
+ this.#expirePending();
111
+ this.#binder.refresh();
112
+ }, PENDING_ECO_MAX_MS);
113
+ // Homebridge owns the process lifetime; this must not hold Node open.
114
+ this.#pendingTimer.unref?.();
74
115
  }
75
116
  #bindSwitch() {
76
117
  const { Characteristic, Service: HapService } = this.#platform;
77
118
  const service = this.#accessory.getService(HapService.Switch)
78
- ?? this.#accessory.addService(HapService.Switch, DISPLAY_NAME);
79
- service.setCharacteristic(Characteristic.Name, DISPLAY_NAME);
119
+ ?? this.#accessory.addService(HapService.Switch, settings_1.GLOBAL_ECO_DISPLAY_NAME);
120
+ service.setCharacteristic(Characteristic.Name, settings_1.GLOBAL_ECO_DISPLAY_NAME);
80
121
  this.#accessory
81
122
  .setPrimaryService?.(service);
82
123
  this.#binder.bind(service, Characteristic.On, () => this.#allEco, {
@@ -91,22 +132,21 @@ class GlobalEcoAccessory {
91
132
  if (!sent) {
92
133
  this.#clearPending();
93
134
  if (!this.#platform.resolvedConfig.allowThermostatControl) {
94
- this.#log.warn(`${DISPLAY_NAME}: ignoring HomeKit change — enable Allow thermostat control in config.`);
135
+ this.#log.warn(`${settings_1.GLOBAL_ECO_DISPLAY_NAME}: ignoring HomeKit change — enable Allow thermostat control in config.`);
95
136
  }
96
137
  // HAP assigns On after onSet; revert to Nest-derived `#allEco`.
97
138
  setImmediate(() => this.#binder.refresh());
98
139
  return;
99
140
  }
100
- this.#log.info(`${DISPLAY_NAME}: ${ecoOn ? 'Updating all thermostats to Eco' : 'Clearing Eco on all thermostats'}`);
141
+ this.#log.info(`${settings_1.GLOBAL_ECO_DISPLAY_NAME}: ${ecoOn ? 'Updating all thermostats to Eco' : 'Clearing Eco on all thermostats'}`);
101
142
  // Optimistic until Nest's all-Eco aggregate matches; do not refresh here
102
143
  // or HAP's post-onSet value is overwritten by a stale read.
103
- this.#pendingAllEco = ecoOn;
104
- this.#pendingSinceMs = Date.now();
144
+ this.#armPending(ecoOn);
105
145
  this.#allEco = ecoOn;
106
146
  }
107
147
  catch (error) {
108
148
  this.#clearPending();
109
- this.#log.warn(`${DISPLAY_NAME}: Eco update failed: ${(0, sanitizers_1.sanitizeError)(error)}`);
149
+ this.#log.warn(`${settings_1.GLOBAL_ECO_DISPLAY_NAME}: Eco update failed: ${(0, sanitizers_1.sanitizeError)(error)}`);
110
150
  setImmediate(() => this.#binder.refresh());
111
151
  }
112
152
  }
@@ -116,10 +156,9 @@ class GlobalEcoAccessory {
116
156
  ?? this.#accessory.addService(HapService.AccessoryInformation);
117
157
  service
118
158
  .setCharacteristic(Characteristic.Manufacturer, settings_1.MANUFACTURER)
119
- .setCharacteristic(Characteristic.Name, DISPLAY_NAME)
159
+ .setCharacteristic(Characteristic.Name, settings_1.GLOBAL_ECO_DISPLAY_NAME)
120
160
  .setCharacteristic(Characteristic.Model, 'Eco Mode')
121
- .setCharacteristic(Characteristic.SerialNumber, 'GLOBAL_ECO');
161
+ .setCharacteristic(Characteristic.SerialNumber, settings_1.GLOBAL_ECO_DEVICE_ID);
122
162
  }
123
163
  }
124
164
  exports.GlobalEcoAccessory = GlobalEcoAccessory;
125
- //# sourceMappingURL=global-eco.js.map
@@ -42,4 +42,3 @@ export declare class ProtectAccessory extends NestAccessory<ProtectState> {
42
42
  protected onServicesMayChange(): void;
43
43
  protected describeState(): string;
44
44
  }
45
- //# sourceMappingURL=protect.d.ts.map
@@ -66,11 +66,18 @@ class ProtectAccessory extends base_1.NestAccessory {
66
66
  * Nest has said anything would show a working all-clear on no evidence.
67
67
  * Once created, services stay through REST outages (rooms/automations keep
68
68
  * their targets) and are marked inactive/faulted while the feed is stale.
69
+ *
70
+ * Each sensor is gated on *its own* reading. Nest returns different topaz
71
+ * subsets per firmware, and a bucket carrying `smoke_status` without
72
+ * `co_status` must not produce a CO tile: its reader would return undefined,
73
+ * the binder would skip it, and HomeKit would show a CO detector resting at
74
+ * HAP's default "levels normal" with nothing behind it.
69
75
  */
70
76
  #ensureAlarmServices() {
71
77
  const { Characteristic, Service: HapService } = this.platform;
72
- const hasAlarms = this.state.smoke !== undefined || this.state.carbonMonoxide !== undefined;
73
- if (!hasAlarms) {
78
+ const hasSmoke = this.state.smoke !== undefined;
79
+ const hasCarbonMonoxide = this.state.carbonMonoxide !== undefined;
80
+ if (!hasSmoke && !hasCarbonMonoxide) {
74
81
  this.removeService(HapService.SmokeSensor);
75
82
  this.removeService(HapService.CarbonMonoxideSensor);
76
83
  this.#smokeService = null;
@@ -92,25 +99,53 @@ class ProtectAccessory extends base_1.NestAccessory {
92
99
  this.#didLogAlarmFeedStale = false;
93
100
  this.log.info(`${this.identity.name}: REST alarm feed restored — Smoke/CO live again.`);
94
101
  }
95
- this.#smokeService = this.resolveService(HapService.SmokeSensor);
96
- this.#smokeService.setCharacteristic(Characteristic.Name, `${this.identity.name} Smoke`);
97
- this.#coService = this.resolveService(HapService.CarbonMonoxideSensor);
98
- this.#coService.setCharacteristic(Characteristic.Name, `${this.identity.name} CO`);
99
- this.binder.bind(this.#smokeService, Characteristic.SmokeDetected, () => this.#toSmokeValue(this.state.smoke));
100
- this.binder.bind(this.#coService, Characteristic.CarbonMonoxideDetected, () => this.#toCarbonMonoxideValue(this.state.carbonMonoxide));
101
- // Both services carry the shared health characteristics so either tile in
102
- // the Home app shows a flat battery, an offline device, or a stale REST feed.
102
+ if (hasSmoke) {
103
+ this.#smokeService = this.resolveService(HapService.SmokeSensor);
104
+ this.#smokeService.setCharacteristic(Characteristic.Name, `${this.identity.name} Smoke`);
105
+ this.binder.bind(this.#smokeService, Characteristic.SmokeDetected, () => this.#toSmokeValue(this.state.smoke));
106
+ }
107
+ else {
108
+ this.removeService(HapService.SmokeSensor);
109
+ this.#smokeService = null;
110
+ }
111
+ if (hasCarbonMonoxide) {
112
+ this.#coService = this.resolveService(HapService.CarbonMonoxideSensor);
113
+ this.#coService.setCharacteristic(Characteristic.Name, `${this.identity.name} CO`);
114
+ this.binder.bind(this.#coService, Characteristic.CarbonMonoxideDetected, () => this.#toCarbonMonoxideValue(this.state.carbonMonoxide));
115
+ }
116
+ else {
117
+ this.removeService(HapService.CarbonMonoxideSensor);
118
+ this.#coService = null;
119
+ }
120
+ // Whichever tiles exist carry the shared health characteristics, so the
121
+ // Home app shows a flat battery, an offline device, or a stale REST feed.
103
122
  for (const service of [this.#smokeService, this.#coService]) {
104
- this.binder.bind(service, Characteristic.StatusLowBattery, () => this.#toLowBatteryValue());
105
- this.binder.bind(service, Characteristic.StatusActive, () => this.#isAlarmReadingLive());
106
- this.binder.bind(service, Characteristic.StatusFault, () => this.#isAlarmReadingLive()
107
- ? Characteristic.StatusFault.NO_FAULT
108
- : Characteristic.StatusFault.GENERAL_FAULT);
123
+ if (service) {
124
+ this.#bindHealth(service);
125
+ }
109
126
  }
110
127
  // Alarm tiles carry battery; the standalone Battery service is only for
111
128
  // Observe-only Protects that have no smoke/CO yet.
112
129
  this.removeService(HapService.Battery);
113
130
  }
131
+ /** Battery plus liveness, for the alarm tiles. */
132
+ #bindHealth(service) {
133
+ this.binder.bind(service, this.platform.Characteristic.StatusLowBattery, () => this.#toLowBatteryValue());
134
+ this.#bindLiveness(service);
135
+ }
136
+ /**
137
+ * Mark a service inactive and faulted whenever its reading is not live.
138
+ *
139
+ * Shared by the alarm tiles and the occupancy tile: all three are backed by
140
+ * the same REST feed, so they go stale together.
141
+ */
142
+ #bindLiveness(service) {
143
+ const { Characteristic } = this.platform;
144
+ this.binder.bind(service, Characteristic.StatusActive, () => this.#isAlarmReadingLive());
145
+ this.binder.bind(service, Characteristic.StatusFault, () => this.#isAlarmReadingLive()
146
+ ? Characteristic.StatusFault.NO_FAULT
147
+ : Characteristic.StatusFault.GENERAL_FAULT);
148
+ }
114
149
  /** Alarm readings are live only while online and the REST feed is fresh. */
115
150
  #isAlarmReadingLive() {
116
151
  return this.state.isOnline !== false && this.state.isAlarmFeedStale !== true;
@@ -184,10 +219,7 @@ class ProtectAccessory extends base_1.NestAccessory {
184
219
  : this.state.isOccupied
185
220
  ? Characteristic.OccupancyDetected.OCCUPANCY_DETECTED
186
221
  : Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED);
187
- this.binder.bind(service, Characteristic.StatusActive, () => this.#isAlarmReadingLive());
188
- this.binder.bind(service, Characteristic.StatusFault, () => this.#isAlarmReadingLive()
189
- ? Characteristic.StatusFault.NO_FAULT
190
- : Characteristic.StatusFault.GENERAL_FAULT);
222
+ this.#bindLiveness(service);
191
223
  if (!_a.#didLogOccupancyHint) {
192
224
  _a.#didLogOccupancyHint = true;
193
225
  this.log.info(`Protect occupancy is Nest ~${settings_1.PROTECT_OCCUPANCY_HOLD_OFF_SEC / 60}-min presence (auto_away), not motion.`);
@@ -251,13 +283,7 @@ class ProtectAccessory extends base_1.NestAccessory {
251
283
  : Characteristic.CarbonMonoxideDetected.CO_LEVELS_ABNORMAL;
252
284
  }
253
285
  #toLowBatteryValue() {
254
- const { Characteristic } = this.platform;
255
- if (this.state.isBatteryLow === undefined) {
256
- return undefined;
257
- }
258
- return this.state.isBatteryLow
259
- ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW
260
- : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL;
286
+ return this.toLowBatteryValue(this.state.isBatteryLow);
261
287
  }
262
288
  /**
263
289
  * HomeKit wants 0–100; Nest REST reports millivolts as `battery_level`.
@@ -293,4 +319,3 @@ class ProtectAccessory extends base_1.NestAccessory {
293
319
  }
294
320
  exports.ProtectAccessory = ProtectAccessory;
295
321
  _a = ProtectAccessory;
296
- //# sourceMappingURL=protect.js.map
@@ -25,4 +25,3 @@ export declare class TemperatureSensorAccessory extends NestAccessory<Temperatur
25
25
  protected bindCharacteristics(): void;
26
26
  protected describeState(): string;
27
27
  }
28
- //# sourceMappingURL=temperature-sensor.d.ts.map
@@ -36,13 +36,7 @@ class TemperatureSensorAccessory extends base_1.NestAccessory {
36
36
  this.binder.bind(battery, Characteristic.StatusLowBattery, () => this.#toLowBatteryValue());
37
37
  }
38
38
  #toLowBatteryValue() {
39
- const { Characteristic } = this.platform;
40
- if (this.state.isBatteryLow === undefined) {
41
- return undefined;
42
- }
43
- return this.state.isBatteryLow
44
- ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW
45
- : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL;
39
+ return this.toLowBatteryValue(this.state.isBatteryLow);
46
40
  }
47
41
  describeState() {
48
42
  const parts = [];
@@ -59,4 +53,3 @@ class TemperatureSensorAccessory extends base_1.NestAccessory {
59
53
  }
60
54
  }
61
55
  exports.TemperatureSensorAccessory = TemperatureSensorAccessory;
62
- //# sourceMappingURL=temperature-sensor.js.map