homebridge-mynest 2.1.2 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
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.3](https://github.com/tbaur/homebridge-mynest/compare/v2.1.2...v2.1.3) (2026-10-26)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * log a Nest Observe stream reset at debug ([bc58dae](https://github.com/tbaur/homebridge-mynest/commit/bc58dae903350a101ea9372212199cb6965d80ff))
11
+
5
12
  ## [2.1.2](https://github.com/tbaur/homebridge-mynest/compare/v2.1.1...v2.1.2) (2026-10-26)
6
13
 
7
14
 
@@ -30,12 +30,19 @@ export type ObserveEndReason =
30
30
  | 'idle'
31
31
  /** Nest closed the response. */
32
32
  | 'ended'
33
+ /** Nest reset the HTTP/2 stream. Recover the same way as a clean close. */
34
+ | 'reset'
33
35
  /** The caller asked to stop. */
34
36
  | 'aborted';
35
37
  export interface ObserveSessionResult {
36
38
  readonly reason: ObserveEndReason;
37
39
  readonly frameCount: number;
38
40
  readonly durationMs: number;
41
+ /**
42
+ * nghttp2 code and request id, set only for {@link ObserveEndReason} `reset`.
43
+ * A clean close has no detail.
44
+ */
45
+ readonly detail?: string;
39
46
  }
40
47
  /** Node's `http2.connect`, or a substitute supplied by tests. */
41
48
  export type Http2Connect = typeof http2.connect;
@@ -39,6 +39,32 @@ const MAX_HANDLER_FAILURE_WARNINGS = 3;
39
39
  const errors_1 = require("../errors");
40
40
  const framing_1 = require("./framing");
41
41
  const protobuf_1 = require("./protobuf");
42
+ /**
43
+ * Peer resets that mean this stream is over.
44
+ *
45
+ * Nest drains a live Observe stream with `RST_STREAM` `INTERNAL_ERROR`. Node
46
+ * reports that as "Stream closed with error code NGHTTP2_INTERNAL_ERROR".
47
+ * A stream that already delivered data clears the failure streak, so rejecting
48
+ * here made every recycle log "failed 1x in a row" at warning level. A clean
49
+ * `end` for the same recycle stays at debug. `NO_ERROR`, `CANCEL`,
50
+ * `REFUSED_STREAM`, and `STREAM_CLOSED` are the same kind of close. Protocol
51
+ * and flow-control errors still fail the session.
52
+ */
53
+ const PEER_RESET = /(?:Stream|Session) closed with error code (NGHTTP2_(?:NO_ERROR|INTERNAL_ERROR|STREAM_CLOSED|REFUSED_STREAM|CANCEL))\b/;
54
+ /**
55
+ * Classify a socket error as a peer reset or a real stream failure.
56
+ *
57
+ * @see PEER_RESET
58
+ */
59
+ function classifySocketError(error, requestId, label) {
60
+ const code = PEER_RESET.exec(error.message)?.[1];
61
+ if (code !== undefined) {
62
+ return { reason: 'reset', detail: `${code} [request-id ${requestId}]` };
63
+ }
64
+ return {
65
+ error: new errors_1.ObserveStreamError(`${label} [request-id ${requestId}]: ${error.message}`, { cause: error }),
66
+ };
67
+ }
42
68
  /**
43
69
  * Headers the Nest gateway requires on an Observe request.
44
70
  *
@@ -95,9 +121,9 @@ function runObserveSession(options) {
95
121
  const client = connect(options.endpoints.grpcOrigin, { maxOutstandingPings: 2 });
96
122
  // Registered before anything can throw: an unhandled `error` on an
97
123
  // Http2Session is a hard process crash.
98
- client.on('error', (error) => settle({
99
- error: new errors_1.ObserveStreamError(`Observe connection failed [request-id ${requestId}]: ${error.message}`, { cause: error }),
100
- }));
124
+ client.on('error', (error) => {
125
+ settle(classifySocketError(error, requestId, 'Observe connection failed'));
126
+ });
101
127
  let request;
102
128
  try {
103
129
  request = client.request(observeHeaders(options.session, options.endpoints, requestId));
@@ -160,6 +186,7 @@ function runObserveSession(options) {
160
186
  reason: outcome.reason,
161
187
  frameCount,
162
188
  durationMs: Date.now() - startedAt,
189
+ ...(outcome.detail !== undefined ? { detail: outcome.detail } : {}),
163
190
  });
164
191
  };
165
192
  const onAbort = () => settle({ reason: 'aborted' });
@@ -272,9 +299,9 @@ function runObserveSession(options) {
272
299
  }
273
300
  });
274
301
  request.on('end', () => settle({ reason: 'ended' }));
275
- request.on('error', (error) => settle({
276
- error: new errors_1.ObserveStreamError(`Observe stream failed [request-id ${requestId}]: ${error.message}`, { cause: error }),
277
- }));
302
+ request.on('error', (error) => {
303
+ settle(classifySocketError(error, requestId, 'Observe stream failed'));
304
+ });
278
305
  client.on('close', () => settle({ reason: 'ended' }));
279
306
  resetIdleTimer();
280
307
  request.end(traitsRequest);
@@ -540,7 +540,8 @@ class NestTransport {
540
540
  else {
541
541
  consecutiveFailures++;
542
542
  }
543
- this.#options.log.debug(`Observe stream ended (${result.reason}) after ${result.frameCount} frame(s) in ${Math.round(result.durationMs / 1000)}s`);
543
+ const detail = result.detail === undefined ? '' : ` (${result.detail})`;
544
+ this.#options.log.debug(`Observe stream ended (${result.reason}) after ${result.frameCount} frame(s) in ${Math.round(result.durationMs / 1000)}s${detail}`);
544
545
  }
545
546
  catch (error) {
546
547
  this.#observeSessionOpen = false;
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.2",
4
+ "version": "2.1.3",
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",