homebridge-mynest 2.1.0 → 2.1.1

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 CHANGED
@@ -2,6 +2,14 @@
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.1.1](https://github.com/tbaur/homebridge-mynest/compare/v2.1.0...v2.1.1) (2026-10-26)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * log raw Protect REST fields at debug, not on every arrival ([#69](https://github.com/tbaur/homebridge-mynest/issues/69)) ([44c7cd9](https://github.com/tbaur/homebridge-mynest/commit/44c7cd95c6502047eaf3b9501a861d3085af8c85))
11
+ * stop faulting temperature sensors when the house is quiet ([#71](https://github.com/tbaur/homebridge-mynest/issues/71)) ([793601c](https://github.com/tbaur/homebridge-mynest/commit/793601c9a673d7ad5debf6eac220c473f6683387))
12
+
5
13
  ## [2.1.0](https://github.com/tbaur/homebridge-mynest/compare/v2.0.2...v2.1.0) (2026-10-26)
6
14
 
7
15
 
package/README.md CHANGED
@@ -108,8 +108,8 @@ The [detailed documentation](docs/README-DETAILED.md#full-configuration-referenc
108
108
  1. **Authentication error:** token missing, truncated, Google JWT/`ya29.`, or revoked. Capture a fresh Nest Account token ([docs/AUTH.md](docs/AUTH.md)).
109
109
  2. **Thermostat missing:** modern thermostats are Observe-only on some accounts. Enable `debug` for stream detail.
110
110
  3. **Eco / setpoint changes snap back:** enable **Allow thermostat control**.
111
- 4. **Protect smoke/CO shows a fault:** REST has not supplied that sensor's number, or the REST feed is stale. The tile stays. A line `REST topaz auto_away=... smoke_status=... co_status=...` is the raw reading, including when the summary sentence did not change.
112
- 5. **No occupancy, or a room that stays Occupied:** occupancy is Nest's `auto_away` inverted (false means Occupied), not motion. It is omitted for a battery Protect, when `auto_away` is missing, or when `exposeProtectOccupancy` is off. A stuck Occupied is that boolean staying false. The `REST topaz` line shows the raw value each time a Protect object arrives.
111
+ 4. **Protect smoke/CO shows a fault:** REST has not supplied that sensor's number, or the REST feed is stale. The tile stays. With `debug` on, a `REST topaz` line shows the raw `smoke_status` and `co_status` Nest sent.
112
+ 5. **No occupancy, or a room that stays Occupied:** occupancy is Nest's `auto_away` inverted (false means Occupied), not motion. It is omitted for a battery Protect, when `auto_away` is missing, or when `exposeProtectOccupancy` is off. A stuck Occupied is that boolean staying false. The Protect's summary line, such as `Zone One Protect: Smoke ok, CO ok, Occupied`, shows the change when it happens.
113
113
 
114
114
  The [full troubleshooting list](docs/README-DETAILED.md#troubleshooting) covers more cases, including a stuck Home app tile.
115
115
 
@@ -28,7 +28,7 @@ class TemperatureSensorAccessory extends base_1.NestAccessory {
28
28
  // No temperature yet is not a live 0 °C. HAP's default for this
29
29
  // characteristic is 0, and 0 is inside the legal range, so the fault is
30
30
  // what tells Home the number is not a reading. A later real 0 °C clears it.
31
- this.binder.bind(this.#service, Characteristic.StatusFault, () => (this.state.isObserveStale === true || this.state.temperatureC === undefined || this.isUnreported
31
+ this.binder.bind(this.#service, Characteristic.StatusFault, () => (this.state.temperatureC === undefined || this.isUnreported
32
32
  ? Characteristic.StatusFault.GENERAL_FAULT
33
33
  : Characteristic.StatusFault.NO_FAULT));
34
34
  this.binder.bind(this.#service, Characteristic.CurrentTemperature, () => this.state.temperatureC);
@@ -33,11 +33,6 @@ export interface NestTransportOptions {
33
33
  * on). Absent on callers that only have the merged map.
34
34
  */
35
35
  onBuckets: (buckets: BucketMap, updatedKeys?: readonly string[]) => void;
36
- /**
37
- * Called when decoded Observe device traits have been missing for
38
- * {@link OBSERVE_DEVICE_DATA_STALE_MS}, and again when one arrives.
39
- */
40
- onObserveDeviceData?: (stale: boolean) => void;
41
36
  /**
42
37
  * Called when a new Observe connection is about to open.
43
38
  *
@@ -78,11 +73,6 @@ export interface NestTransportOptions {
78
73
  statusHeartbeatMs?: number;
79
74
  observeSilenceCheckMs?: number;
80
75
  forbiddenReprobeMs?: number;
81
- /**
82
- * How long with no decoded device trait before temperature sensors fault.
83
- * Defaults to {@link OBSERVE_DEVICE_DATA_STALE_MS}. Tests set a short value.
84
- */
85
- observeDataStaleMs?: number;
86
76
  /** Injected REST subscribe breaker (tests); defaults to a fresh instance. */
87
77
  restCircuitBreaker?: CircuitBreaker;
88
78
  /** Injected `app_launch` breaker (tests); defaults to a fresh instance. */
@@ -74,8 +74,6 @@ class NestTransport {
74
74
  #lastObserveFrameAt = null;
75
75
  /** Decoded `DEVICE_` traits in the Observe session that is open now. */
76
76
  #decodedDeviceTraitsThisSession = 0;
77
- #observeDataStale = false;
78
- #observeDataStaleTimer = null;
79
77
  #loopRestartTimers = new Map();
80
78
  #observeSessionOpen = false;
81
79
  #restLoopRunning = false;
@@ -251,7 +249,6 @@ class NestTransport {
251
249
  // default `--unhandled-rejections=throw` — taking every other plugin down.
252
250
  this.#startLoop('Observe', () => this.#runObserveLoop());
253
251
  this.#startLoop('REST', () => this.#runSubscribeLoop());
254
- this.#scheduleObserveDataStale();
255
252
  if (this.#options.statusHeartbeatEnabled !== false) {
256
253
  this.#startStatusHeartbeat();
257
254
  }
@@ -331,10 +328,6 @@ class NestTransport {
331
328
  this.#observeStartupWarnTimer = null;
332
329
  }
333
330
  this.#clearRestAlarmFeedStaleTimer();
334
- if (this.#observeDataStaleTimer) {
335
- clearTimeout(this.#observeDataStaleTimer);
336
- this.#observeDataStaleTimer = null;
337
- }
338
331
  for (const timer of this.#loopRestartTimers.values()) {
339
332
  clearTimeout(timer);
340
333
  }
@@ -647,45 +640,15 @@ class NestTransport {
647
640
  if (status?.code !== undefined && status.code !== 0) {
648
641
  this.#options.log.debug(`Observe stream reported status ${status.code}${status.message ? `: ${status.message}` : ''}`);
649
642
  }
643
+ // Observe sends device traits only on change, so a quiet house can go a
644
+ // long time without one. This counts them only to judge a session productive.
650
645
  if (traits.some((trait) => trait.resourceId.startsWith(classify_1.OBSERVE_DEVICE_PREFIX))) {
651
- this.#noteDecodedDeviceTrait();
646
+ this.#decodedDeviceTraitsThisSession++;
652
647
  }
653
648
  if (traits.length > 0) {
654
649
  this.#options.onTraits(traits);
655
650
  }
656
651
  }
657
- /** A decoded device trait is the only Observe event that means the house is live. */
658
- #noteDecodedDeviceTrait() {
659
- this.#decodedDeviceTraitsThisSession++;
660
- if (this.#observeDataStale) {
661
- this.#observeDataStale = false;
662
- this.#options.log.info('Observe: device updates resumed');
663
- this.#options.onObserveDeviceData?.(false);
664
- }
665
- this.#scheduleObserveDataStale();
666
- }
667
- /**
668
- * Mark temperature sensors faulted if this long passes with no decoded
669
- * device trait. Thermostats keep their last values.
670
- *
671
- * Catalog frames still increment `#observeFrames` and would otherwise keep
672
- * the stream looking healthy.
673
- */
674
- #scheduleObserveDataStale() {
675
- if (this.#observeDataStaleTimer) {
676
- clearTimeout(this.#observeDataStaleTimer);
677
- }
678
- this.#observeDataStaleTimer = setTimeout(() => {
679
- this.#observeDataStaleTimer = null;
680
- if (this.#isStopped || this.#observeDataStale) {
681
- return;
682
- }
683
- this.#observeDataStale = true;
684
- this.#options.log.warn('Observe: no device updates for 10 min, temperature sensors marked faulted');
685
- this.#options.onObserveDeviceData?.(true);
686
- }, this.#options.observeDataStaleMs ?? settings_1.OBSERVE_DEVICE_DATA_STALE_MS);
687
- this.#observeDataStaleTimer.unref?.();
688
- }
689
652
  /**
690
653
  * Run a transport loop, and start it again if it throws.
691
654
  *
package/dist/platform.js CHANGED
@@ -119,8 +119,6 @@ class MyNestPlatform {
119
119
  #pendingUpdate = null;
120
120
  #pendingChangedIds = null;
121
121
  #bucketsChanged = false;
122
- /** True after 10 minutes with no decoded Observe device trait. */
123
- #observeDeviceDataStale = false;
124
122
  #isShuttingDown = false;
125
123
  #hasFatal = false;
126
124
  #startedAt = 0;
@@ -264,11 +262,6 @@ class MyNestPlatform {
264
262
  log: this.#log,
265
263
  onTraits: (traits) => this.#applyTraits(traits),
266
264
  onBuckets: (buckets, updatedKeys) => this.#applyBuckets(buckets, updatedKeys),
267
- onObserveDeviceData: (stale) => {
268
- this.#observeDeviceDataStale = stale;
269
- this.#bucketsChanged = true;
270
- this.#scheduleUpdate();
271
- },
272
265
  onObserveSessionStart: () => this.#beginObserveSnapshot(),
273
266
  onFatal: (error) => this.#handleFatal(error),
274
267
  onCircuitOpen: () => diagnostics?.breakerTrip(),
@@ -592,7 +585,7 @@ class MyNestPlatform {
592
585
  const topaz = value;
593
586
  const knownName = this.#lastInventory?.protects.get(id)?.identity.name;
594
587
  const description = (0, classify_1.cleanName)(topaz.description);
595
- this.#log.info((0, protect_state_1.formatTopazArrival)(topaz, knownName ?? description ?? id));
588
+ this.#log.debug((0, protect_state_1.formatTopazArrival)(topaz, knownName ?? description ?? id));
596
589
  }
597
590
  }
598
591
  /** Collapse a burst of transport updates into one HomeKit refresh. */
@@ -633,7 +626,6 @@ class MyNestPlatform {
633
626
  buckets: this.#buckets,
634
627
  ignoredDeviceIds: this.#config.ignoredDeviceIds,
635
628
  restAlarmFeedAvailable: isFeedAvailable,
636
- observeDataStale: this.#observeDeviceDataStale,
637
629
  });
638
630
  this.#lastInventory = inventory;
639
631
  // A reconnect snapshot names every device; a typical patch names one.
@@ -1146,7 +1138,6 @@ class MyNestPlatform {
1146
1138
  buckets: this.#buckets,
1147
1139
  ignoredDeviceIds: this.#config.ignoredDeviceIds,
1148
1140
  restAlarmFeedAvailable: this.#transport?.status.isRestAlarmFeedAvailable ?? false,
1149
- observeDataStale: this.#observeDeviceDataStale,
1150
1141
  })
1151
1142
  : null);
1152
1143
  if (inventory) {
@@ -175,15 +175,6 @@ export declare const OBSERVE_CONNECT_TIMEOUT_MS = 30000;
175
175
  * the failure mode that leaves HomeKit showing stale temperatures indefinitely.
176
176
  */
177
177
  export declare const OBSERVE_IDLE_TIMEOUT_MS: number;
178
- /**
179
- * How long Observe may go without a decoded device trait before temperature
180
- * sensors are marked StatusFault.
181
- *
182
- * Thermostats do not get StatusFault in this HAP build. They keep their last
183
- * values, and the transport logs the gap. Catalog frames and a socket that
184
- * stays open do not count.
185
- */
186
- export declare const OBSERVE_DEVICE_DATA_STALE_MS: number;
187
178
  /** Delay before the first reconnect attempt after a transport drops. */
188
179
  export declare const RECONNECT_BASE_MS = 5000;
189
180
  /** Upper bound on the reconnect backoff. */
package/dist/settings.js CHANGED
@@ -15,8 +15,8 @@
15
15
  * nothing external will tell you when it changes.
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.SETPOINT_STEP_C = exports.MAX_SETPOINT_C = exports.MIN_SETPOINT_C = exports.PROTECT_OCCUPANCY_HOLD_OFF_SEC = exports.MAX_DIAGNOSTICS_INTERVAL_SEC = exports.MIN_DIAGNOSTICS_INTERVAL_SEC = exports.REDISCOVERY_INTERVAL_MS = exports.MAX_RESPONSE_BYTES = exports.MIN_RETRY_AFTER_MS = exports.MAX_RETRY_AFTER_MS = exports.OBSERVE_SILENCE_CHECK_MS = exports.FRAME_DECODE_FAILURE_RATIO = exports.FRAME_DECODE_WINDOW = exports.STATUS_HEARTBEAT_MS = exports.LOOP_FAILURE_WARN_EVERY = exports.BATCH_UPDATE_MAX_ATTEMPTS = exports.MAX_REQUEST_ATTEMPTS = exports.SESSION_EXPIRY_MARGIN_MS = exports.SESSION_REFRESH_MS = exports.FORBIDDEN_REPROBE_MS = exports.FORBIDDEN_FATAL_THRESHOLD = exports.OBSERVE_SNAPSHOT_ABANDON_MS = exports.OBSERVE_SNAPSHOT_SETTLE_MS = exports.OBSERVE_STARTUP_WARN_MS = exports.MIN_SUBSCRIBE_CYCLE_MS = exports.RECONNECT_MAX_MS = exports.RECONNECT_BASE_MS = exports.OBSERVE_DEVICE_DATA_STALE_MS = exports.OBSERVE_IDLE_TIMEOUT_MS = exports.OBSERVE_CONNECT_TIMEOUT_MS = exports.OBSERVE_PING_INTERVAL_MS = exports.OBSERVE_SESSION_MS = exports.REST_RESPONSE_STALE_MS = exports.REST_PROOF_OF_LIFE_MS = exports.REST_ALARM_FEED_STALE_MS = exports.SUBSCRIBE_IDLE_MIN_ELAPSED_RATIO = exports.SUBSCRIBE_TIMEOUT_MS = exports.THERMOSTAT_WRITE_ECHO_MAX_MS = exports.BATCH_UPDATE_TIMEOUT_MS = exports.APP_LAUNCH_TIMEOUT_MS = exports.SESSION_TIMEOUT_MS = exports.APP_LAUNCH_BUCKET_TYPES = exports.WEB_APP_VERSION = exports.USER_AGENT = exports.MANUFACTURER = exports.GLOBAL_ECO_DISPLAY_NAME = exports.GLOBAL_ECO_DEVICE_ID = exports.UUID_PREFIX = exports.PLATFORM_NAME = exports.PLUGIN_NAME = void 0;
19
- exports.REPORTED_TEMPERATURE_STEP_C = exports.MAX_REPORTED_TEMPERATURE_C = exports.MIN_REPORTED_TEMPERATURE_C = exports.MIN_SETPOINT_SPAN_C = exports.DEFAULT_SETPOINT_SPAN_C = exports.DEFAULT_SETPOINT_C = void 0;
18
+ exports.DEFAULT_SETPOINT_C = exports.SETPOINT_STEP_C = exports.MAX_SETPOINT_C = exports.MIN_SETPOINT_C = exports.PROTECT_OCCUPANCY_HOLD_OFF_SEC = exports.MAX_DIAGNOSTICS_INTERVAL_SEC = exports.MIN_DIAGNOSTICS_INTERVAL_SEC = exports.REDISCOVERY_INTERVAL_MS = exports.MAX_RESPONSE_BYTES = exports.MIN_RETRY_AFTER_MS = exports.MAX_RETRY_AFTER_MS = exports.OBSERVE_SILENCE_CHECK_MS = exports.FRAME_DECODE_FAILURE_RATIO = exports.FRAME_DECODE_WINDOW = exports.STATUS_HEARTBEAT_MS = exports.LOOP_FAILURE_WARN_EVERY = exports.BATCH_UPDATE_MAX_ATTEMPTS = exports.MAX_REQUEST_ATTEMPTS = exports.SESSION_EXPIRY_MARGIN_MS = exports.SESSION_REFRESH_MS = exports.FORBIDDEN_REPROBE_MS = exports.FORBIDDEN_FATAL_THRESHOLD = exports.OBSERVE_SNAPSHOT_ABANDON_MS = exports.OBSERVE_SNAPSHOT_SETTLE_MS = exports.OBSERVE_STARTUP_WARN_MS = exports.MIN_SUBSCRIBE_CYCLE_MS = exports.RECONNECT_MAX_MS = exports.RECONNECT_BASE_MS = exports.OBSERVE_IDLE_TIMEOUT_MS = exports.OBSERVE_CONNECT_TIMEOUT_MS = exports.OBSERVE_PING_INTERVAL_MS = exports.OBSERVE_SESSION_MS = exports.REST_RESPONSE_STALE_MS = exports.REST_PROOF_OF_LIFE_MS = exports.REST_ALARM_FEED_STALE_MS = exports.SUBSCRIBE_IDLE_MIN_ELAPSED_RATIO = exports.SUBSCRIBE_TIMEOUT_MS = exports.THERMOSTAT_WRITE_ECHO_MAX_MS = exports.BATCH_UPDATE_TIMEOUT_MS = exports.APP_LAUNCH_TIMEOUT_MS = exports.SESSION_TIMEOUT_MS = exports.APP_LAUNCH_BUCKET_TYPES = exports.WEB_APP_VERSION = exports.USER_AGENT = exports.MANUFACTURER = exports.GLOBAL_ECO_DISPLAY_NAME = exports.GLOBAL_ECO_DEVICE_ID = exports.UUID_PREFIX = exports.PLATFORM_NAME = exports.PLUGIN_NAME = void 0;
19
+ exports.REPORTED_TEMPERATURE_STEP_C = exports.MAX_REPORTED_TEMPERATURE_C = exports.MIN_REPORTED_TEMPERATURE_C = exports.MIN_SETPOINT_SPAN_C = exports.DEFAULT_SETPOINT_SPAN_C = void 0;
20
20
  exports.resolveEndpoints = resolveEndpoints;
21
21
  exports.appLaunchUrl = appLaunchUrl;
22
22
  /** Name used to register the plugin with Homebridge (must match package.json name). */
@@ -198,15 +198,6 @@ exports.OBSERVE_CONNECT_TIMEOUT_MS = 30_000;
198
198
  * the failure mode that leaves HomeKit showing stale temperatures indefinitely.
199
199
  */
200
200
  exports.OBSERVE_IDLE_TIMEOUT_MS = 10 * 60_000;
201
- /**
202
- * How long Observe may go without a decoded device trait before temperature
203
- * sensors are marked StatusFault.
204
- *
205
- * Thermostats do not get StatusFault in this HAP build. They keep their last
206
- * values, and the transport logs the gap. Catalog frames and a socket that
207
- * stays open do not count.
208
- */
209
- exports.OBSERVE_DEVICE_DATA_STALE_MS = exports.OBSERVE_IDLE_TIMEOUT_MS;
210
201
  /** Delay before the first reconnect attempt after a transport drops. */
211
202
  exports.RECONNECT_BASE_MS = 5_000;
212
203
  /** Upper bound on the reconnect backoff. */
@@ -72,11 +72,10 @@ export declare function readProtectState(options: {
72
72
  restAlarmFeedAvailable?: boolean;
73
73
  }): ProtectState;
74
74
  /**
75
- * One info line for a REST Protect object, including when HomeKit's sentence
76
- * would be unchanged.
75
+ * One debug line with the raw REST fields behind a Protect's summary.
77
76
  *
78
- * `auto_away` false is Occupied. A stuck Occupied is invisible if the plugin
79
- * only logs the summary when that word changes.
77
+ * `auto_away` false is Occupied. The accessory's info summary already shows
78
+ * the readable state on change; this is for checking what Nest actually sent.
80
79
  */
81
80
  export declare function formatTopazArrival(topaz: TopazBucket, label: string): string;
82
81
  /** A user-facing explanation for why occupancy is or is not published. */
@@ -175,11 +175,10 @@ function readPlausibleRestTemperature(value) {
175
175
  : undefined;
176
176
  }
177
177
  /**
178
- * One info line for a REST Protect object, including when HomeKit's sentence
179
- * would be unchanged.
178
+ * One debug line with the raw REST fields behind a Protect's summary.
180
179
  *
181
- * `auto_away` false is Occupied. A stuck Occupied is invisible if the plugin
182
- * only logs the summary when that word changes.
180
+ * `auto_away` false is Occupied. The accessory's info summary already shows
181
+ * the readable state on change; this is for checking what Nest actually sent.
183
182
  */
184
183
  function formatTopazArrival(topaz, label) {
185
184
  const autoAway = typeof topaz.auto_away === 'boolean' ? String(topaz.auto_away) : 'absent';
@@ -27,12 +27,6 @@ export interface BuildInventoryOptions {
27
27
  * values. Defaults to true.
28
28
  */
29
29
  restAlarmFeedAvailable?: boolean;
30
- /**
31
- * When true, `isObserveStale` is set on thermostats and temperature sensors.
32
- * Only temperature sensors publish Status Fault. Thermostats keep their last
33
- * values. Defaults to false.
34
- */
35
- observeDataStale?: boolean;
36
30
  }
37
31
  /**
38
32
  * Merge both transports into the device list the platform publishes.
@@ -36,7 +36,7 @@ const REST_DEVICE_BUCKETS = [
36
36
  * be exercised against fixtures without any transport.
37
37
  */
38
38
  function buildInventory(options) {
39
- const { observe, buckets, ignoredDeviceIds, restAlarmFeedAvailable = true, observeDataStale = false, } = options;
39
+ const { observe, buckets, ignoredDeviceIds, restAlarmFeedAvailable = true, } = options;
40
40
  const kinds = collectDeviceKinds(observe, buckets);
41
41
  const roomNames = mergeRoomNames((0, classify_1.collectObserveRoomNames)(observe), readRestRoomNames(buckets));
42
42
  const comfortTemperatures = readComfortTemperatures(observe, buckets, kinds);
@@ -50,7 +50,6 @@ function buildInventory(options) {
50
50
  deviceId,
51
51
  roomNames,
52
52
  restAlarmFeedAvailable,
53
- observeDataStale,
54
53
  };
55
54
  let device;
56
55
  switch (kind) {
@@ -236,10 +235,9 @@ function buildThermostat(context, comfortTemperatureC) {
236
235
  ? (0, thermostat_state_1.readThermostatFromObserve)(observe, resourceId, { comfortTemperatureC })
237
236
  : undefined;
238
237
  const fromRest = (0, thermostat_state_1.readThermostatFromRest)(buckets.shared?.[deviceId], buckets.device?.[deviceId]);
239
- const state = (0, thermostat_state_1.mergeThermostatState)(fromObserve, fromRest);
240
238
  return {
241
239
  identity: buildIdentity(context, 'thermostat'),
242
- state: context.observeDataStale ? { ...state, isObserveStale: true } : state,
240
+ state: (0, thermostat_state_1.mergeThermostatState)(fromObserve, fromRest),
243
241
  };
244
242
  }
245
243
  function buildProtect(context) {
@@ -256,14 +254,13 @@ function buildProtect(context) {
256
254
  }
257
255
  function buildTemperatureSensor(context) {
258
256
  const { observe, buckets, deviceId } = context;
259
- const state = (0, sensor_state_1.readTemperatureSensorState)({
260
- state: observe,
261
- resourceId: (0, classify_1.toResourceId)(deviceId),
262
- kryptonite: buckets.kryptonite?.[deviceId],
263
- });
264
257
  return {
265
258
  identity: buildIdentity(context, 'temperature_sensor'),
266
- state: context.observeDataStale ? { ...state, isObserveStale: true } : state,
259
+ state: (0, sensor_state_1.readTemperatureSensorState)({
260
+ state: observe,
261
+ resourceId: (0, classify_1.toResourceId)(deviceId),
262
+ kryptonite: buckets.kryptonite?.[deviceId],
263
+ }),
267
264
  };
268
265
  }
269
266
  /** Every device in an inventory, in a stable order, for logging and iteration. */
@@ -71,11 +71,6 @@ export interface ThermostatState {
71
71
  * Celsius. `TemperatureDisplayUnits` follows this field.
72
72
  */
73
73
  readonly displayUnit?: 'C' | 'F';
74
- /**
75
- * True when Observe has produced no decoded device trait for 10 minutes.
76
- * Last values stay published.
77
- */
78
- readonly isObserveStale?: boolean;
79
74
  }
80
75
  /** Nest's three-level alarm scale, shared by the smoke and CO sensors. */
81
76
  export type AlarmLevel = 'ok' | 'warning' | 'emergency';
@@ -137,8 +132,6 @@ export interface TemperatureSensorState {
137
132
  /** Percent, when Nest reports one. */
138
133
  readonly batteryLevel?: number;
139
134
  readonly isBatteryLow?: boolean;
140
- /** See {@link ThermostatState.isObserveStale}. */
141
- readonly isObserveStale?: boolean;
142
135
  }
143
136
  /** One device, discriminated by {@link DeviceKind}. */
144
137
  export type NestDevice = {
@@ -84,7 +84,7 @@ Unknown power is not a reason to omit occupancy when `auto_away` is already a bo
84
84
 
85
85
  If REST later goes stale, a last occupancy reading stays published but is marked inactive and faulted.
86
86
 
87
- Every REST object for a Protect logs one info line, `REST topaz auto_away=... smoke_status=... co_status=...`, including when the HomeKit summary sentence did not change. `absent` means Nest did not send that field.
87
+ Each Protect logs a summary at info when its state changes, such as `Zone One Protect: Smoke ok, CO ok, Occupied`. With `debug` on, every REST object for a Protect also logs the raw fields, `REST topaz auto_away=... smoke_status=... co_status=...`. `absent` means Nest did not send that field.
88
88
 
89
89
  ## Smoke and CO honesty
90
90
 
@@ -142,8 +142,8 @@ Each signal covers the whole house. The summary does not name the Protect, so ch
142
142
  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).
143
143
  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.
144
144
  4. **Eco / setpoint changes snap back.** Enable **Allow thermostat control**. With control off, HomeKit can still move the UI (required for tiles), but the plugin does not send the change to Nest and moves the tile back.
145
- 5. **Protect smoke or CO shows a fault.** REST has no number for that sensor yet, or the REST feed is stale. The tile stays. Search the log for `REST topaz` to see the raw `smoke_status` and `co_status`.
146
- 6. **No occupancy, or Occupied in an empty room.** Occupancy is `auto_away` inverted. It is omitted for a battery Protect, a missing `auto_away`, an Observe-only Protect, or `exposeProtectOccupancy` off. A room that stays Occupied has `auto_away=false` on each later `REST topaz` line, or no later line at all. See [Protect occupancy](#protect-occupancy).
145
+ 5. **Protect smoke or CO shows a fault.** REST has no number for that sensor yet, or the REST feed is stale. The tile stays. Turn on `debug` and search the log for `REST topaz` to see the raw `smoke_status` and `co_status`.
146
+ 6. **No occupancy, or Occupied in an empty room.** Occupancy is `auto_away` inverted. It is omitted for a battery Protect, a missing `auto_away`, an Observe-only Protect, or `exposeProtectOccupancy` off. A room that stays Occupied never logs a later summary with `Unoccupied`. With `debug` on, each `REST topaz` line for it shows `auto_away=false`. See [Protect occupancy](#protect-occupancy).
147
147
 
148
148
  Restart Homebridge after any `config.json` edit.
149
149
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-mynest",
3
3
  "displayName": "Homebridge My Nest",
4
- "version": "2.1.0",
4
+ "version": "2.1.1",
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",