homebridge-mynest 2.1.0 → 2.1.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 +15 -0
- package/README.md +2 -2
- package/dist/accessories/temperature-sensor.js +1 -1
- package/dist/api/session.d.ts +1 -1
- package/dist/api/session.js +25 -9
- package/dist/api/transport.d.ts +0 -10
- package/dist/api/transport.js +3 -40
- package/dist/errors/index.d.ts +16 -2
- package/dist/errors/index.js +18 -3
- package/dist/nest-signals.d.ts +5 -3
- package/dist/nest-signals.js +16 -9
- package/dist/platform.js +40 -19
- package/dist/settings.d.ts +0 -9
- package/dist/settings.js +2 -11
- package/dist/state/protect-state.d.ts +3 -4
- package/dist/state/protect-state.js +3 -4
- package/dist/state/registry.d.ts +0 -6
- package/dist/state/registry.js +7 -10
- package/dist/types/device.d.ts +0 -7
- package/docs/PROTOCOL.md +1 -1
- package/docs/README-DETAILED.md +6 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
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.2](https://github.com/tbaur/homebridge-mynest/compare/v2.1.1...v2.1.2) (2026-10-26)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* page auth-failed when Nest rejects the token ([#72](https://github.com/tbaur/homebridge-mynest/issues/72)) ([d69bf69](https://github.com/tbaur/homebridge-mynest/commit/d69bf69e1e67e4f75e8a5148edcbd2c933499ee4))
|
|
11
|
+
|
|
12
|
+
## [2.1.1](https://github.com/tbaur/homebridge-mynest/compare/v2.1.0...v2.1.1) (2026-10-26)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* 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))
|
|
18
|
+
* 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))
|
|
19
|
+
|
|
5
20
|
## [2.1.0](https://github.com/tbaur/homebridge-mynest/compare/v2.0.2...v2.1.0) (2026-10-26)
|
|
6
21
|
|
|
7
22
|
|
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.
|
|
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
|
|
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.
|
|
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);
|
package/dist/api/session.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export declare function authenticatedHeaders(session: NestSession): Record<strin
|
|
|
33
33
|
/**
|
|
34
34
|
* Exchange the configured access token for a usable session.
|
|
35
35
|
*
|
|
36
|
-
* @throws {AuthenticationError} When Nest rejects the token.
|
|
36
|
+
* @throws {AuthenticationError} When Nest rejects the token (HTTP 400 or 401).
|
|
37
37
|
* @throws {SessionShapeError} When the response is accepted but omits a field
|
|
38
38
|
* the plugin needs, or names a transport host off Nest infrastructure. Either
|
|
39
39
|
* means Nest changed the contract; a new token would not help.
|
package/dist/api/session.js
CHANGED
|
@@ -48,26 +48,42 @@ function authenticatedHeaders(session) {
|
|
|
48
48
|
'Content-Type': 'application/json',
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Fetch `GET /session`, reporting HTTP 400 or 401 as a rejected token.
|
|
53
|
+
*
|
|
54
|
+
* @throws {AuthenticationError} On HTTP 400 or 401.
|
|
55
|
+
*/
|
|
56
|
+
async function requestSession(options) {
|
|
57
|
+
try {
|
|
58
|
+
return await (0, http_1.requestJson)(options.endpoints.sessionUrl, {
|
|
59
|
+
method: 'GET',
|
|
60
|
+
headers: sessionHeaders(options.accessToken),
|
|
61
|
+
timeoutMs: settings_1.SESSION_TIMEOUT_MS,
|
|
62
|
+
fetchImpl: options.fetchImpl,
|
|
63
|
+
signal: options.signal,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (!(0, errors_1.isSessionTokenRejection)(error) || error instanceof errors_1.AuthenticationError) {
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
throw new errors_1.AuthenticationError(error.message, { cause: error, status: error.httpStatus });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
51
73
|
/**
|
|
52
74
|
* Exchange the configured access token for a usable session.
|
|
53
75
|
*
|
|
54
|
-
* @throws {AuthenticationError} When Nest rejects the token.
|
|
76
|
+
* @throws {AuthenticationError} When Nest rejects the token (HTTP 400 or 401).
|
|
55
77
|
* @throws {SessionShapeError} When the response is accepted but omits a field
|
|
56
78
|
* the plugin needs, or names a transport host off Nest infrastructure. Either
|
|
57
79
|
* means Nest changed the contract; a new token would not help.
|
|
58
80
|
*/
|
|
59
81
|
async function openSession(options) {
|
|
60
|
-
const { accessToken,
|
|
82
|
+
const { accessToken, log } = options;
|
|
61
83
|
if (log.debugEnabled) {
|
|
62
84
|
log.debug(`Opening a Nest session with the configured token ${(0, sanitizers_1.previewSecret)(accessToken)}`);
|
|
63
85
|
}
|
|
64
|
-
const body = await (
|
|
65
|
-
method: 'GET',
|
|
66
|
-
headers: sessionHeaders(accessToken),
|
|
67
|
-
timeoutMs: settings_1.SESSION_TIMEOUT_MS,
|
|
68
|
-
fetchImpl: options.fetchImpl,
|
|
69
|
-
signal: options.signal,
|
|
70
|
-
});
|
|
86
|
+
const body = await requestSession(options);
|
|
71
87
|
const token = typeof body.access_token === 'string' ? body.access_token : undefined;
|
|
72
88
|
// Only a scalar. `String()` accepts anything, so an object would become
|
|
73
89
|
// `'[object Object]'` — truthy, so it passes the missing-field check below and
|
package/dist/api/transport.d.ts
CHANGED
|
@@ -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. */
|
package/dist/api/transport.js
CHANGED
|
@@ -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.#
|
|
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/errors/index.d.ts
CHANGED
|
@@ -28,7 +28,8 @@ export declare class ConfigurationError extends NestError {
|
|
|
28
28
|
readonly isRetryable = false;
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* Nest rejected the access token
|
|
31
|
+
* Nest rejected the access token: HTTP 401 anywhere, or HTTP 400 from the
|
|
32
|
+
* session endpoint (see {@link isSessionTokenRejection}).
|
|
32
33
|
*
|
|
33
34
|
* Not retryable: the token is copied by hand from a browser session and only a
|
|
34
35
|
* new one will help. Retrying a rejected token is also how an account attracts
|
|
@@ -37,11 +38,24 @@ export declare class ConfigurationError extends NestError {
|
|
|
37
38
|
export declare class AuthenticationError extends NestError {
|
|
38
39
|
readonly code = "AUTH_ERROR";
|
|
39
40
|
readonly isRetryable = false;
|
|
40
|
-
readonly httpStatus
|
|
41
|
+
readonly httpStatus: number;
|
|
41
42
|
constructor(message?: string, options?: {
|
|
42
43
|
cause?: Error;
|
|
44
|
+
status?: number;
|
|
43
45
|
});
|
|
44
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Whether a failed session open proves the configured token is unusable.
|
|
49
|
+
*
|
|
50
|
+
* The session endpoint takes nothing but the token, and Nest answers a
|
|
51
|
+
* missing or bad one with 400 (`invalid_request`) or 401. Every other failure
|
|
52
|
+
* retries, because a 403 or an unfamiliar code can be an edge block that
|
|
53
|
+
* clears on its own. Retrying never hides an outage: the alarm feed goes stale
|
|
54
|
+
* and alarm-feed-stale pages whatever the cause.
|
|
55
|
+
*/
|
|
56
|
+
export declare function isSessionTokenRejection(error: unknown): error is NestError & {
|
|
57
|
+
httpStatus: number;
|
|
58
|
+
};
|
|
45
59
|
/**
|
|
46
60
|
* Nest answered HTTP 403 Forbidden.
|
|
47
61
|
*
|
package/dist/errors/index.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
exports.CircuitBreakerError = exports.ObserveStreamError = exports.ApiParseError = exports.ApiResponseError = exports.RateLimitError = exports.TimeoutError = exports.NetworkError = exports.SessionShapeError = exports.ForbiddenError = exports.AuthenticationError = exports.ConfigurationError = exports.NestError = void 0;
|
|
12
|
+
exports.isSessionTokenRejection = isSessionTokenRejection;
|
|
12
13
|
exports.isFatalTransportError = isFatalTransportError;
|
|
13
14
|
exports.isCircuitBreakerFailure = isCircuitBreakerFailure;
|
|
14
15
|
exports.parseRetryAfterMs = parseRetryAfterMs;
|
|
@@ -42,7 +43,8 @@ class ConfigurationError extends NestError {
|
|
|
42
43
|
}
|
|
43
44
|
exports.ConfigurationError = ConfigurationError;
|
|
44
45
|
/**
|
|
45
|
-
* Nest rejected the access token
|
|
46
|
+
* Nest rejected the access token: HTTP 401 anywhere, or HTTP 400 from the
|
|
47
|
+
* session endpoint (see {@link isSessionTokenRejection}).
|
|
46
48
|
*
|
|
47
49
|
* Not retryable: the token is copied by hand from a browser session and only a
|
|
48
50
|
* new one will help. Retrying a rejected token is also how an account attracts
|
|
@@ -51,12 +53,25 @@ exports.ConfigurationError = ConfigurationError;
|
|
|
51
53
|
class AuthenticationError extends NestError {
|
|
52
54
|
code = 'AUTH_ERROR';
|
|
53
55
|
isRetryable = false;
|
|
54
|
-
httpStatus
|
|
56
|
+
httpStatus;
|
|
55
57
|
constructor(message = 'Nest rejected the configured access token. Sign in at https://home.nest.com, open https://home.nest.com/session, and copy a fresh "access_token".', options) {
|
|
56
|
-
super(message, options);
|
|
58
|
+
super(message, options?.cause ? { cause: options.cause } : undefined);
|
|
59
|
+
this.httpStatus = options?.status ?? 401;
|
|
57
60
|
}
|
|
58
61
|
}
|
|
59
62
|
exports.AuthenticationError = AuthenticationError;
|
|
63
|
+
/**
|
|
64
|
+
* Whether a failed session open proves the configured token is unusable.
|
|
65
|
+
*
|
|
66
|
+
* The session endpoint takes nothing but the token, and Nest answers a
|
|
67
|
+
* missing or bad one with 400 (`invalid_request`) or 401. Every other failure
|
|
68
|
+
* retries, because a 403 or an unfamiliar code can be an edge block that
|
|
69
|
+
* clears on its own. Retrying never hides an outage: the alarm feed goes stale
|
|
70
|
+
* and alarm-feed-stale pages whatever the cause.
|
|
71
|
+
*/
|
|
72
|
+
function isSessionTokenRejection(error) {
|
|
73
|
+
return error instanceof NestError && (error.httpStatus === 400 || error.httpStatus === 401);
|
|
74
|
+
}
|
|
60
75
|
/**
|
|
61
76
|
* Nest answered HTTP 403 Forbidden.
|
|
62
77
|
*
|
package/dist/nest-signals.d.ts
CHANGED
|
@@ -47,10 +47,12 @@ export declare class NestSignals {
|
|
|
47
47
|
#private;
|
|
48
48
|
constructor(writer: SignalWriter, graceMs?: number);
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
50
|
+
* Write auth-failed and alarm-feed-stale clear, and start the alarm feed grace.
|
|
51
51
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
52
|
+
* Call once, before the first request. The write lists this plugin in
|
|
53
|
+
* homebridge-pagerduty even if Nest never answers, and the grace timer pages
|
|
54
|
+
* for a feed that is already down at launch. Smoke and CO wait for a fresh
|
|
55
|
+
* feed, so a restart cannot clear a real alarm.
|
|
54
56
|
*/
|
|
55
57
|
start(): void;
|
|
56
58
|
/** Nest accepted the token. Clears auth-failed. */
|
package/dist/nest-signals.js
CHANGED
|
@@ -58,16 +58,20 @@ class NestSignals {
|
|
|
58
58
|
this.#graceMs = graceMs;
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
61
|
+
* Write auth-failed and alarm-feed-stale clear, and start the alarm feed grace.
|
|
62
62
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
63
|
+
* Call once, before the first request. The write lists this plugin in
|
|
64
|
+
* homebridge-pagerduty even if Nest never answers, and the grace timer pages
|
|
65
|
+
* for a feed that is already down at launch. Smoke and CO wait for a fresh
|
|
66
|
+
* feed, so a restart cannot clear a real alarm.
|
|
65
67
|
*/
|
|
66
68
|
start() {
|
|
67
|
-
if (this.#isClosed || this.#hasStarted) {
|
|
69
|
+
if (this.#isClosed || this.#hasStarted || this.#hasStopped) {
|
|
68
70
|
return;
|
|
69
71
|
}
|
|
70
72
|
this.#hasStarted = true;
|
|
73
|
+
this.#writeAuthFailedClear();
|
|
74
|
+
this.#writeFeedStale(false);
|
|
71
75
|
this.#armStaleTimer();
|
|
72
76
|
}
|
|
73
77
|
/** Nest accepted the token. Clears auth-failed. */
|
|
@@ -75,11 +79,7 @@ class NestSignals {
|
|
|
75
79
|
if (this.#isClosed || this.#hasStopped) {
|
|
76
80
|
return;
|
|
77
81
|
}
|
|
78
|
-
this.#
|
|
79
|
-
active: false,
|
|
80
|
-
severity: 'error',
|
|
81
|
-
summary: exports.TOKEN_REJECTED_SUMMARY,
|
|
82
|
-
});
|
|
82
|
+
this.#writeAuthFailedClear();
|
|
83
83
|
}
|
|
84
84
|
/** Record the Protects HomeKit is showing and whether their alarm feed is fresh. */
|
|
85
85
|
update(protects, isFeedAvailable) {
|
|
@@ -132,6 +132,13 @@ class NestSignals {
|
|
|
132
132
|
this.#clearStaleTimer();
|
|
133
133
|
this.#writer.close();
|
|
134
134
|
}
|
|
135
|
+
#writeAuthFailedClear() {
|
|
136
|
+
this.#writer.write(exports.SIGNAL_AUTH_FAILED, {
|
|
137
|
+
active: false,
|
|
138
|
+
severity: 'error',
|
|
139
|
+
summary: exports.TOKEN_REJECTED_SUMMARY,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
135
142
|
#writeFeedStale(active) {
|
|
136
143
|
this.#writer.write(exports.SIGNAL_ALARM_FEED_STALE, {
|
|
137
144
|
active,
|
package/dist/platform.js
CHANGED
|
@@ -66,7 +66,32 @@ const UPDATE_COALESCE_MS = 250;
|
|
|
66
66
|
* only remaining signal that HomeKit's values are frozen.
|
|
67
67
|
*/
|
|
68
68
|
const FATAL_REMINDER_MS = 60 * 60_000;
|
|
69
|
-
|
|
69
|
+
function tokenRejectedMessage(status) {
|
|
70
|
+
return `Nest rejected the token (HTTP ${status}). Get a new token from https://home.nest.com/session and restart Homebridge.`;
|
|
71
|
+
}
|
|
72
|
+
/** A few words for the startup retry line; the full error goes to debug. */
|
|
73
|
+
function describeStartupFailure(error) {
|
|
74
|
+
if (error instanceof errors_1.CircuitBreakerError) {
|
|
75
|
+
return 'paused after repeated failures';
|
|
76
|
+
}
|
|
77
|
+
if (error instanceof errors_1.TimeoutError) {
|
|
78
|
+
return 'timed out';
|
|
79
|
+
}
|
|
80
|
+
if (error instanceof errors_1.NetworkError) {
|
|
81
|
+
return 'network error';
|
|
82
|
+
}
|
|
83
|
+
if (error instanceof errors_1.ApiParseError) {
|
|
84
|
+
return 'unexpected response';
|
|
85
|
+
}
|
|
86
|
+
if (error instanceof errors_1.NestError && error.httpStatus !== undefined) {
|
|
87
|
+
return `HTTP ${error.httpStatus}`;
|
|
88
|
+
}
|
|
89
|
+
return (0, sanitizers_1.sanitizeError)(error);
|
|
90
|
+
}
|
|
91
|
+
function formatDelay(ms) {
|
|
92
|
+
const seconds = Math.round(ms / 1000);
|
|
93
|
+
return seconds < 120 ? `${seconds}s` : `${Math.round(seconds / 60)} min`;
|
|
94
|
+
}
|
|
70
95
|
function stopReasonFor(error) {
|
|
71
96
|
if (error instanceof errors_1.SessionShapeError) {
|
|
72
97
|
return 'session';
|
|
@@ -119,8 +144,6 @@ class MyNestPlatform {
|
|
|
119
144
|
#pendingUpdate = null;
|
|
120
145
|
#pendingChangedIds = null;
|
|
121
146
|
#bucketsChanged = false;
|
|
122
|
-
/** True after 10 minutes with no decoded Observe device trait. */
|
|
123
|
-
#observeDeviceDataStale = false;
|
|
124
147
|
#isShuttingDown = false;
|
|
125
148
|
#hasFatal = false;
|
|
126
149
|
#startedAt = 0;
|
|
@@ -205,6 +228,9 @@ class MyNestPlatform {
|
|
|
205
228
|
for (const warning of warnings) {
|
|
206
229
|
this.#log.warn(warning);
|
|
207
230
|
}
|
|
231
|
+
if (resolved.allowThermostatControl) {
|
|
232
|
+
this.#log.info('Thermostat control enabled');
|
|
233
|
+
}
|
|
208
234
|
}
|
|
209
235
|
catch (error) {
|
|
210
236
|
// Homebridge keeps running other platforms, so this must not throw. The
|
|
@@ -254,9 +280,6 @@ class MyNestPlatform {
|
|
|
254
280
|
this.#holdUnpublishedProtectAlarms();
|
|
255
281
|
this.#signals?.start();
|
|
256
282
|
this.#startedAt = Date.now();
|
|
257
|
-
if (this.#config.allowThermostatControl) {
|
|
258
|
-
this.#log.info('Thermostat control enabled');
|
|
259
|
-
}
|
|
260
283
|
const diagnostics = this.#diagnostics;
|
|
261
284
|
const transport = new transport_1.NestTransport({
|
|
262
285
|
accessToken: this.#config.accessToken,
|
|
@@ -264,11 +287,6 @@ class MyNestPlatform {
|
|
|
264
287
|
log: this.#log,
|
|
265
288
|
onTraits: (traits) => this.#applyTraits(traits),
|
|
266
289
|
onBuckets: (buckets, updatedKeys) => this.#applyBuckets(buckets, updatedKeys),
|
|
267
|
-
onObserveDeviceData: (stale) => {
|
|
268
|
-
this.#observeDeviceDataStale = stale;
|
|
269
|
-
this.#bucketsChanged = true;
|
|
270
|
-
this.#scheduleUpdate();
|
|
271
|
-
},
|
|
272
290
|
onObserveSessionStart: () => this.#beginObserveSnapshot(),
|
|
273
291
|
onFatal: (error) => this.#handleFatal(error),
|
|
274
292
|
onCircuitOpen: () => diagnostics?.breakerTrip(),
|
|
@@ -309,8 +327,8 @@ class MyNestPlatform {
|
|
|
309
327
|
if (this.#isShuttingDown) {
|
|
310
328
|
return;
|
|
311
329
|
}
|
|
312
|
-
this.#log.
|
|
313
|
-
this.#scheduleStartRetry();
|
|
330
|
+
this.#log.debug(`Startup failed: ${(0, sanitizers_1.sanitizeError)(error)}`);
|
|
331
|
+
this.#scheduleStartRetry(describeStartupFailure(error));
|
|
314
332
|
return;
|
|
315
333
|
}
|
|
316
334
|
this.#startRetryAttempt = 0;
|
|
@@ -327,7 +345,7 @@ class MyNestPlatform {
|
|
|
327
345
|
* the plugin permanently idle — and cache-restored Protect tiles keep whatever
|
|
328
346
|
* smoke/CO value Homebridge persisted, with no handler to mark them faulted.
|
|
329
347
|
*/
|
|
330
|
-
#scheduleStartRetry() {
|
|
348
|
+
#scheduleStartRetry(reason) {
|
|
331
349
|
if (this.#isShuttingDown || this.#hasFatal || this.#startRetryTimer) {
|
|
332
350
|
return;
|
|
333
351
|
}
|
|
@@ -336,6 +354,7 @@ class MyNestPlatform {
|
|
|
336
354
|
this.#holdUnpublishedProtectAlarms();
|
|
337
355
|
this.#startRetryAttempt++;
|
|
338
356
|
const delayMs = (0, retry_1.computeBackoffMs)(this.#startRetryAttempt);
|
|
357
|
+
this.#log.warn(`Could not connect to Nest (${reason}), retrying in ${formatDelay(delayMs)}`);
|
|
339
358
|
this.#startRetryTimer = setTimeout(() => {
|
|
340
359
|
this.#startRetryTimer = null;
|
|
341
360
|
if (this.#isShuttingDown || this.#hasFatal) {
|
|
@@ -400,7 +419,11 @@ class MyNestPlatform {
|
|
|
400
419
|
}
|
|
401
420
|
this.#hasFatal = true;
|
|
402
421
|
const isSessionShape = error instanceof errors_1.SessionShapeError;
|
|
403
|
-
|
|
422
|
+
const message = error instanceof errors_1.AuthenticationError
|
|
423
|
+
? tokenRejectedMessage(error.httpStatus)
|
|
424
|
+
: error.message;
|
|
425
|
+
this.#log.error(message);
|
|
426
|
+
this.#log.debug(`Stopped by: ${(0, sanitizers_1.sanitizeError)(error)}`);
|
|
404
427
|
this.#signals?.noteStopped(stopReasonFor(error));
|
|
405
428
|
// Mark Protect smoke/CO inactive/faulted while handlers can still refresh.
|
|
406
429
|
// `#stop` sets `#isShuttingDown` and drops `#scheduleUpdate`, so the
|
|
@@ -424,7 +447,7 @@ class MyNestPlatform {
|
|
|
424
447
|
// the single error line above scrolls out of the log within hours.
|
|
425
448
|
const reminder = isSessionShape
|
|
426
449
|
? 'Still stopped: unsupported Nest session format. Readings are frozen.'
|
|
427
|
-
: `Still stopped: ${
|
|
450
|
+
: `Still stopped: ${message}`;
|
|
428
451
|
this.#fatalReminderTimer = setInterval(() => {
|
|
429
452
|
this.#log.error(reminder);
|
|
430
453
|
}, FATAL_REMINDER_MS);
|
|
@@ -592,7 +615,7 @@ class MyNestPlatform {
|
|
|
592
615
|
const topaz = value;
|
|
593
616
|
const knownName = this.#lastInventory?.protects.get(id)?.identity.name;
|
|
594
617
|
const description = (0, classify_1.cleanName)(topaz.description);
|
|
595
|
-
this.#log.
|
|
618
|
+
this.#log.debug((0, protect_state_1.formatTopazArrival)(topaz, knownName ?? description ?? id));
|
|
596
619
|
}
|
|
597
620
|
}
|
|
598
621
|
/** Collapse a burst of transport updates into one HomeKit refresh. */
|
|
@@ -633,7 +656,6 @@ class MyNestPlatform {
|
|
|
633
656
|
buckets: this.#buckets,
|
|
634
657
|
ignoredDeviceIds: this.#config.ignoredDeviceIds,
|
|
635
658
|
restAlarmFeedAvailable: isFeedAvailable,
|
|
636
|
-
observeDataStale: this.#observeDeviceDataStale,
|
|
637
659
|
});
|
|
638
660
|
this.#lastInventory = inventory;
|
|
639
661
|
// A reconnect snapshot names every device; a typical patch names one.
|
|
@@ -1146,7 +1168,6 @@ class MyNestPlatform {
|
|
|
1146
1168
|
buckets: this.#buckets,
|
|
1147
1169
|
ignoredDeviceIds: this.#config.ignoredDeviceIds,
|
|
1148
1170
|
restAlarmFeedAvailable: this.#transport?.status.isRestAlarmFeedAvailable ?? false,
|
|
1149
|
-
observeDataStale: this.#observeDeviceDataStale,
|
|
1150
1171
|
})
|
|
1151
1172
|
: null);
|
|
1152
1173
|
if (inventory) {
|
package/dist/settings.d.ts
CHANGED
|
@@ -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.
|
|
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 =
|
|
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
|
|
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.
|
|
79
|
-
*
|
|
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
|
|
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.
|
|
182
|
-
*
|
|
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';
|
package/dist/state/registry.d.ts
CHANGED
|
@@ -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.
|
package/dist/state/registry.js
CHANGED
|
@@ -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,
|
|
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:
|
|
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:
|
|
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. */
|
package/dist/types/device.d.ts
CHANGED
|
@@ -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 = {
|
package/docs/PROTOCOL.md
CHANGED
|
@@ -29,7 +29,7 @@ HomeKit accessories are unregistered only when Nest has confirmed a device is go
|
|
|
29
29
|
| Observe never connected this session | Do not unregister (REST-only view cannot prove thermostats are gone) |
|
|
30
30
|
| Cached accessory not named by Nest since the restart | Two settled Observe snapshots that both omit it before prune |
|
|
31
31
|
| Device absent from the Observe ∪ REST inventory after the above | Unregister from HomeKit |
|
|
32
|
-
| Expired or
|
|
32
|
+
| Expired, revoked or missing access token (HTTP 401 anywhere, HTTP 400 from `/session`, or a session this plugin cannot use) | Stop updates; **keep** accessories. Any other failure, including HTTP 403 from either host, retries and does not stop the plugin, including when both transports exhaust their budget. Each transport probes again after the cooldown. Nest Account auth is a manually pasted session token, so unregistering would bounce rooms and automations until the user pastes a fresh token and restarts |
|
|
33
33
|
|
|
34
34
|
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.
|
|
35
35
|
|
package/docs/README-DETAILED.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
|
@@ -131,10 +131,10 @@ Each condition below then opens a PagerDuty alert while it is present, and resol
|
|
|
131
131
|
| --- | --- | --- |
|
|
132
132
|
| `smoke` | critical | Any Protect reports a smoke warning or emergency |
|
|
133
133
|
| `co` | critical | Any Protect reports a carbon monoxide warning or emergency |
|
|
134
|
-
| `alarm-feed-stale` | error | The Protect alarm feed has been unavailable for 5 minutes, so HomeKit may not show a smoke or CO alarm |
|
|
135
|
-
| `auth-failed` | error | The plugin stopped because Nest rejected the token, the token in the config is missing or invalid, or Nest changed its session format |
|
|
134
|
+
| `alarm-feed-stale` | error | The Protect alarm feed has been unavailable for 5 minutes, whatever the cause, so HomeKit may not show a smoke or CO alarm |
|
|
135
|
+
| `auth-failed` | error | The plugin stopped because Nest rejected the token (HTTP 400 or 401), the token in the config is missing or invalid, or Nest changed its session format |
|
|
136
136
|
|
|
137
|
-
Each signal covers the whole house. The summary does not name the Protect, so check the Nest app for which one is alarming. While the alarm feed is stale, `smoke` and `co` keep their last value. They do not clear until fresh readings arrive. While `auth-failed` is active, `alarm-feed-stale` stays off, so a stopped plugin sends one page.
|
|
137
|
+
Each signal covers the whole house. The summary does not name the Protect, so check the Nest app for which one is alarming. While the alarm feed is stale, `smoke` and `co` keep their last value. They do not clear until fresh readings arrive. While `auth-failed` is active, `alarm-feed-stale` stays off, so a stopped plugin sends one page. Any other failure, such as HTTP 403, an unfamiliar error code or a network outage, keeps retrying and pages `alarm-feed-stale` if it lasts 5 minutes. The plugin writes `auth-failed` and `alarm-feed-stale` at startup, so homebridge-pagerduty lists it even if Nest never answers.
|
|
138
138
|
|
|
139
139
|
## Troubleshooting
|
|
140
140
|
|
|
@@ -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.
|
|
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
|
|
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.
|
|
4
|
+
"version": "2.1.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",
|