homebridge-roborock-vacuum 1.6.2 → 1.7.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.
- package/README.md +39 -0
- package/config.schema.json +32 -0
- package/dist/platform.js +1 -0
- package/dist/platform.js.map +1 -1
- package/package.json +1 -1
- package/roborockLib/lib/messageQueueHandler.js +14 -0
- package/roborockLib/lib/vacuum.js +35 -0
- package/roborockLib/roborockAPI.js +176 -0
package/README.md
CHANGED
|
@@ -72,3 +72,42 @@ Follow these steps to install the plugin:
|
|
|
72
72
|
Use the Homebridge UI settings page to sign in and configure the plugin. To exclude vacuums from HomeKit, add their Roborock device IDs to **Skipped Device IDs**.
|
|
73
73
|
|
|
74
74
|
When Homebridge restarts, matching devices will be skipped during discovery. If a skipped device already exists in HomeKit as a cached accessory, the plugin will remove it from Homebridge.
|
|
75
|
+
|
|
76
|
+
## Current Room → MQTT (optional telemetry)
|
|
77
|
+
|
|
78
|
+
The plugin can publish the room a vacuum is currently cleaning to a local MQTT broker, for use in external automations (e.g. lighting that follows the vacuum room to room). This is **telemetry only** — it is not exposed to HomeKit — and is **off by default**. It reuses the `mqtt` dependency the plugin already ships, so it adds nothing when disabled.
|
|
79
|
+
|
|
80
|
+
Enable it under **Current Room → MQTT** in the settings UI, or in `config.json`:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
"currentRoomMqtt": {
|
|
84
|
+
"enabled": true,
|
|
85
|
+
"brokerUrl": "mqtt://127.0.0.1:1883",
|
|
86
|
+
"topic": "homebridge/roborock/{duid}/current_room",
|
|
87
|
+
"cleaningPollSeconds": 10
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- **topic** is a template. `{duid}` and `{name}` (the device name, slugified) are substituted. If the template contains neither token, `/{duid}` is appended automatically so multiple vacuums never publish to the same topic.
|
|
92
|
+
- **cleaningPollSeconds** is how often status is polled while the vacuum is actively cleaning (it polls slowly otherwise). The poll only runs while the feature is enabled.
|
|
93
|
+
|
|
94
|
+
A retained JSON message is published whenever the room changes:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"segment_id": 16,
|
|
99
|
+
"room": "Kitchen",
|
|
100
|
+
"state": 5,
|
|
101
|
+
"target_segment_id": 17,
|
|
102
|
+
"target_room": "Hallway",
|
|
103
|
+
"in_cleaning": 1,
|
|
104
|
+
"ts": 1718524800000
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- **segment_id / room** — the room currently being cleaned. `segment_id` is `-1` (and `room` `null`) when docked/idle, or transiently while the robot relocalizes; use `state` to distinguish.
|
|
109
|
+
- **target_segment_id / target_room** — the next room the robot is heading to (populated during transitions), so a consumer can pre-light it. `-1`/`null` when steady or unknown.
|
|
110
|
+
- **in_cleaning** — the device's own flag; `0` once a clean has concluded even while the robot returns to the dock or empties, which `state` alone does not always distinguish.
|
|
111
|
+
- Room names are resolved from the robot's saved map; name your rooms in the Roborock app for them to appear.
|
|
112
|
+
|
|
113
|
+
> Validated on a Roborock Qrevo (`roborock.vacuum.a185`). On models that don't populate `cleaning_info`, the payload degrades gracefully to `segment_id: -1` / `room: null`.
|
package/config.schema.json
CHANGED
|
@@ -40,6 +40,38 @@
|
|
|
40
40
|
"description": "When enabled, debug messages will be written to the log.",
|
|
41
41
|
"type": "boolean",
|
|
42
42
|
"default": false
|
|
43
|
+
},
|
|
44
|
+
"currentRoomMqtt": {
|
|
45
|
+
"title": "Current Room → MQTT",
|
|
46
|
+
"type": "object",
|
|
47
|
+
"description": "Publish the room each vacuum is currently cleaning to a local MQTT broker (telemetry only; not exposed to HomeKit). Off by default.",
|
|
48
|
+
"properties": {
|
|
49
|
+
"enabled": {
|
|
50
|
+
"title": "Enabled",
|
|
51
|
+
"type": "boolean",
|
|
52
|
+
"default": false,
|
|
53
|
+
"description": "Publish the current cleaning room to MQTT."
|
|
54
|
+
},
|
|
55
|
+
"brokerUrl": {
|
|
56
|
+
"title": "Broker URL",
|
|
57
|
+
"type": "string",
|
|
58
|
+
"default": "mqtt://127.0.0.1:1883",
|
|
59
|
+
"description": "MQTT broker URL."
|
|
60
|
+
},
|
|
61
|
+
"topic": {
|
|
62
|
+
"title": "Topic Template",
|
|
63
|
+
"type": "string",
|
|
64
|
+
"default": "homebridge/roborock/{duid}/current_room",
|
|
65
|
+
"description": "Topic to publish the retained current-room JSON to. Supports {duid} and {name} tokens; if neither is present, /{duid} is appended automatically so multiple vacuums don't collide."
|
|
66
|
+
},
|
|
67
|
+
"cleaningPollSeconds": {
|
|
68
|
+
"title": "Cleaning Poll Interval (s)",
|
|
69
|
+
"type": "integer",
|
|
70
|
+
"default": 10,
|
|
71
|
+
"minimum": 5,
|
|
72
|
+
"description": "How often to poll status while actively cleaning, to catch room changes."
|
|
73
|
+
}
|
|
74
|
+
}
|
|
43
75
|
}
|
|
44
76
|
},
|
|
45
77
|
"required": ["email"]
|
package/dist/platform.js
CHANGED
|
@@ -74,6 +74,7 @@ class RoborockPlatform {
|
|
|
74
74
|
log: this.log,
|
|
75
75
|
userData: decryptedSession,
|
|
76
76
|
storagePath: storagePath,
|
|
77
|
+
currentRoomMqtt: this.platformConfig.currentRoomMqtt,
|
|
77
78
|
});
|
|
78
79
|
/**
|
|
79
80
|
* When this event is fired it means Homebridge has restored all cached accessories from disk.
|
package/dist/platform.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -21,6 +21,14 @@ class messageQueueHandler {
|
|
|
21
21
|
this.adapter.log.info(`Local connection unavailable for ${duid}. Falling back to cloud connection for method ${method}.`);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// Some devices (e.g. the Qrevo / roborock.vacuum.a185) keep the local socket
|
|
25
|
+
// connected but never answer, so the disconnect-based fallback above never
|
|
26
|
+
// triggers. After repeated local timeouts, prefer cloud for a cooldown window.
|
|
27
|
+
if (!useCloudConnection && mqttConnectionState && typeof this.adapter.shouldPreferCloudLocally === "function" && this.adapter.shouldPreferCloudLocally(duid)) {
|
|
28
|
+
useCloudConnection = true;
|
|
29
|
+
this.adapter.log.info(`Local connection for ${duid} is unresponsive. Falling back to cloud connection for method ${method}.`);
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
if (!useCloudConnection && version == "L01") {
|
|
25
33
|
try {
|
|
26
34
|
await this.adapter.localConnector.ensureL01Handshake(duid);
|
|
@@ -64,6 +72,9 @@ class messageQueueHandler {
|
|
|
64
72
|
if (useCloudConnection) {
|
|
65
73
|
reject(new Error(`Cloud request with id ${messageID} with method ${method} timed out after 10 seconds. MQTT connection state: ${mqttConnectionState}`));
|
|
66
74
|
} else {
|
|
75
|
+
if (typeof this.adapter.recordLocalTimeout === "function") {
|
|
76
|
+
this.adapter.recordLocalTimeout(duid);
|
|
77
|
+
}
|
|
67
78
|
reject(new Error(`Local request with id ${messageID} with method ${method} timed out after 10 seconds Local connect state: ${localConnectionState}`));
|
|
68
79
|
}
|
|
69
80
|
}, requestTimeout);
|
|
@@ -73,6 +84,9 @@ class messageQueueHandler {
|
|
|
73
84
|
if (typeof this.adapter.recordMethodSuccess === "function") {
|
|
74
85
|
this.adapter.recordMethodSuccess(duid, method);
|
|
75
86
|
}
|
|
87
|
+
if (!useCloudConnection && typeof this.adapter.recordLocalSuccess === "function") {
|
|
88
|
+
this.adapter.recordLocalSuccess(duid);
|
|
89
|
+
}
|
|
76
90
|
resolve(value);
|
|
77
91
|
};
|
|
78
92
|
|
|
@@ -162,6 +162,38 @@ class vacuum {
|
|
|
162
162
|
const now = new Date();
|
|
163
163
|
const seconds = now.getSeconds();
|
|
164
164
|
|
|
165
|
+
// current-room -> MQTT (opt-in telemetry): a dedicated lightweight poll,
|
|
166
|
+
// independent of the legacy branch below. That branch is gated on
|
|
167
|
+
// `this.adapter.socket` (a live web-UI client) OR an undefined
|
|
168
|
+
// `config.updateInterval` (=> NaN, never true), so it does not run in
|
|
169
|
+
// normal headless operation and can't be relied on to publish. Polls fast
|
|
170
|
+
// while cleaning, idle otherwise; serialized by `_roomPollInFlight`; never
|
|
171
|
+
// runs when the feature is disabled, so default behaviour is unchanged.
|
|
172
|
+
const roomMqttCfg = this.adapter.config && this.adapter.config.currentRoomMqtt;
|
|
173
|
+
if (roomMqttCfg && roomMqttCfg.enabled) {
|
|
174
|
+
const vac = this.adapter.vacuums[duid];
|
|
175
|
+
const idleInterval = this.adapter.updateInterval || 180;
|
|
176
|
+
const fastInterval = roomMqttCfg.cleaningPollSeconds || 10;
|
|
177
|
+
const interval = vac && vac._lastIsCleaning ? Math.min(fastInterval, idleInterval) : idleInterval;
|
|
178
|
+
if (seconds % interval == 0 && !(vac && vac._roomPollInFlight)) {
|
|
179
|
+
if (vac) vac._roomPollInFlight = true;
|
|
180
|
+
try {
|
|
181
|
+
const ds = await this.adapter.messageQueueHandler.sendRequest(duid, "get_prop", ["get_status"]);
|
|
182
|
+
const s0 = (ds && ds[0]) || {};
|
|
183
|
+
if (vac) vac._lastIsCleaning = this.adapter.isCleaning(s0["state"]);
|
|
184
|
+
this.adapter.publishCurrentRoom(duid, {
|
|
185
|
+
cleaningInfo: s0["cleaning_info"],
|
|
186
|
+
state: s0["state"],
|
|
187
|
+
inCleaning: s0["in_cleaning"],
|
|
188
|
+
});
|
|
189
|
+
} catch (e) {
|
|
190
|
+
this.adapter.log.debug(`current_room poll failed: ${e && e.message}`);
|
|
191
|
+
} finally {
|
|
192
|
+
if (vac) vac._roomPollInFlight = false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
165
197
|
if (this.adapter.socket || seconds % this.adapter.config.updateInterval == 0) {
|
|
166
198
|
// only send status every minute or if websocket is connected
|
|
167
199
|
|
|
@@ -257,6 +289,9 @@ class vacuum {
|
|
|
257
289
|
|
|
258
290
|
if (roomName) {
|
|
259
291
|
this.adapter.log.debug(`Mapped room matched: ${roomID} with name: ${roomName}`);
|
|
292
|
+
// current-room -> MQTT: cache segment_id -> name for the publisher.
|
|
293
|
+
if (!this.adapter.segmentRoomNames[duid]) this.adapter.segmentRoomNames[duid] = {};
|
|
294
|
+
this.adapter.segmentRoomNames[duid][mappedRoom[0]] = roomName;
|
|
260
295
|
const objectString = `Devices.${duid}.floors.${roomFloor}.${mappedRoom[0]}`;
|
|
261
296
|
await this.adapter.createStateObjectHelper(objectString, roomName, "boolean", null, true, "value", true, true);
|
|
262
297
|
}
|
|
@@ -21,6 +21,7 @@ const roborockPackageHelper =
|
|
|
21
21
|
const deviceFeatures = require("./lib/deviceFeatures").deviceFeatures;
|
|
22
22
|
const messageQueueHandler =
|
|
23
23
|
require("./lib/messageQueueHandler").messageQueueHandler;
|
|
24
|
+
const mqtt = require("mqtt");
|
|
24
25
|
|
|
25
26
|
let socketServer, webserver;
|
|
26
27
|
|
|
@@ -50,6 +51,12 @@ class Roborock {
|
|
|
50
51
|
this.localKeys = null;
|
|
51
52
|
this.localL01Nonces = new Map();
|
|
52
53
|
this.roomIDs = {};
|
|
54
|
+
// current-room -> MQTT (opt-in telemetry): per-duid segment_id -> room name
|
|
55
|
+
// cache, a lazily-opened local publisher client, and the last-published
|
|
56
|
+
// dedupe key per duid. All inert unless config.currentRoomMqtt.enabled.
|
|
57
|
+
this.segmentRoomNames = {};
|
|
58
|
+
this.localMqttClient = null;
|
|
59
|
+
this._lastRoomPublishKey = {};
|
|
53
60
|
this.vacuums = {};
|
|
54
61
|
this.initializedVacuumDuids = new Set();
|
|
55
62
|
this.socket = null;
|
|
@@ -84,6 +91,15 @@ class Roborock {
|
|
|
84
91
|
this.methodBackoffThreshold = 3; // consecutive timeouts before backing off
|
|
85
92
|
this.methodBackoffInterval = 30 * 60 * 1000; // 30 min before a retry
|
|
86
93
|
|
|
94
|
+
// Some newer models (e.g. the Qrevo / roborock.vacuum.a185) keep the local TCP
|
|
95
|
+
// connection open but never answer, so every local request times out. Track
|
|
96
|
+
// consecutive local timeouts per device and, once over the threshold, prefer the
|
|
97
|
+
// cloud path for a cooldown window (retrying local afterwards). Keyed by duid.
|
|
98
|
+
this.localTimeoutStreak = new Map();
|
|
99
|
+
this.localCloudPreference = new Map();
|
|
100
|
+
this.localFailoverThreshold = 3; // consecutive local timeouts before preferring cloud
|
|
101
|
+
this.localFailoverInterval = 5 * 60 * 1000; // 5 min preferring cloud before retrying local
|
|
102
|
+
|
|
87
103
|
this.localDevices = {};
|
|
88
104
|
this.remoteDevices = new Set();
|
|
89
105
|
|
|
@@ -1268,6 +1284,16 @@ class Roborock {
|
|
|
1268
1284
|
this.clearInterval(this.vacuums[duid].mainUpdateInterval);
|
|
1269
1285
|
}
|
|
1270
1286
|
|
|
1287
|
+
// current-room -> MQTT: tear down the local publisher client.
|
|
1288
|
+
if (this.localMqttClient) {
|
|
1289
|
+
try {
|
|
1290
|
+
this.localMqttClient.end(true);
|
|
1291
|
+
} catch (e) {
|
|
1292
|
+
/* ignore */
|
|
1293
|
+
}
|
|
1294
|
+
this.localMqttClient = null;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1271
1297
|
this.messageQueue.forEach(({ timeout102, timeout301 }) => {
|
|
1272
1298
|
this.clearTimeout(timeout102);
|
|
1273
1299
|
if (timeout301) {
|
|
@@ -1764,6 +1790,117 @@ class Roborock {
|
|
|
1764
1790
|
}
|
|
1765
1791
|
}
|
|
1766
1792
|
|
|
1793
|
+
// current-room -> MQTT: lazily open a LOCAL mqtt publisher (separate from the
|
|
1794
|
+
// Roborock cloud connection). Telemetry only — it never subscribes. Never
|
|
1795
|
+
// throws; a down broker only logs at debug and retries via reconnectPeriod.
|
|
1796
|
+
ensureLocalMqtt() {
|
|
1797
|
+
const cfg = this.config && this.config.currentRoomMqtt;
|
|
1798
|
+
if (!cfg || !cfg.enabled) return null;
|
|
1799
|
+
if (this.localMqttClient) return this.localMqttClient;
|
|
1800
|
+
|
|
1801
|
+
try {
|
|
1802
|
+
const url = cfg.brokerUrl || "mqtt://127.0.0.1:1883";
|
|
1803
|
+
const client = mqtt.connect(url, {
|
|
1804
|
+
reconnectPeriod: 5000,
|
|
1805
|
+
connectTimeout: 10000,
|
|
1806
|
+
});
|
|
1807
|
+
client.on("error", (e) =>
|
|
1808
|
+
this.log.debug(`current_room MQTT error: ${e && e.message}`)
|
|
1809
|
+
);
|
|
1810
|
+
client.on("connect", () =>
|
|
1811
|
+
this.log.debug(`current_room MQTT connected to ${url}`)
|
|
1812
|
+
);
|
|
1813
|
+
// After a reconnect, drop the dedupe cache so the (retained) current room
|
|
1814
|
+
// is re-published — a broker bounce may have lost the retained message.
|
|
1815
|
+
client.on("reconnect", () => {
|
|
1816
|
+
this._lastRoomPublishKey = {};
|
|
1817
|
+
});
|
|
1818
|
+
this.localMqttClient = client;
|
|
1819
|
+
} catch (e) {
|
|
1820
|
+
this.log.debug(`current_room MQTT connect failed: ${e && e.message}`);
|
|
1821
|
+
this.localMqttClient = null;
|
|
1822
|
+
}
|
|
1823
|
+
return this.localMqttClient;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// current-room -> MQTT: resolve the publish topic for a device. The configured
|
|
1827
|
+
// topic is a template: {duid} and {name} tokens are substituted; if it contains
|
|
1828
|
+
// no token, /{duid} is appended so multiple vacuums never collide on one topic.
|
|
1829
|
+
resolveRoomTopic(duid) {
|
|
1830
|
+
const cfg = this.config && this.config.currentRoomMqtt;
|
|
1831
|
+
let topic = (cfg && cfg.topic) || "homebridge/roborock/{duid}/current_room";
|
|
1832
|
+
const hasToken = topic.includes("{duid}") || topic.includes("{name}");
|
|
1833
|
+
if (!hasToken) {
|
|
1834
|
+
topic = `${topic.replace(/\/+$/, "")}/${duid}`;
|
|
1835
|
+
}
|
|
1836
|
+
const rawName = (this.vacuums[duid] && this.vacuums[duid].name) || duid;
|
|
1837
|
+
const slug = String(rawName)
|
|
1838
|
+
.toLowerCase()
|
|
1839
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
1840
|
+
.replace(/^_+|_+$/g, "");
|
|
1841
|
+
// Fall back to duid if the name slugifies to empty (e.g. a symbol-only name),
|
|
1842
|
+
// so a {name}-only template can never collide or produce a malformed topic.
|
|
1843
|
+
const name = slug || duid;
|
|
1844
|
+
return topic.replace(/\{duid\}/g, duid).replace(/\{name\}/g, name);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// current-room -> MQTT: publish the room a vacuum is currently cleaning.
|
|
1848
|
+
// segment_id comes from get_status.cleaning_info; the name is resolved from the
|
|
1849
|
+
// segmentRoomNames cache (populated by the get_room_mapping handler). -1 means
|
|
1850
|
+
// docked/idle or, when state indicates motion, relocalizing. target_* expose the
|
|
1851
|
+
// next room (cleaning_info.target_segment_id populates during transitions) so a
|
|
1852
|
+
// consumer can pre-light it; in_cleaning is the device flag (0 once a clean
|
|
1853
|
+
// concludes, even while it returns/empties). Fire-and-forget; never rejects.
|
|
1854
|
+
publishCurrentRoom(duid, raw) {
|
|
1855
|
+
try {
|
|
1856
|
+
const cfg = this.config && this.config.currentRoomMqtt;
|
|
1857
|
+
if (!cfg || !cfg.enabled) return;
|
|
1858
|
+
const client = this.ensureLocalMqtt();
|
|
1859
|
+
if (!client || !client.connected) return;
|
|
1860
|
+
|
|
1861
|
+
const ci = raw && raw.cleaningInfo;
|
|
1862
|
+
const segId =
|
|
1863
|
+
ci && typeof ci.segment_id === "number" ? ci.segment_id : -1;
|
|
1864
|
+
const room =
|
|
1865
|
+
segId >= 0 ? this.segmentRoomNames?.[duid]?.[segId] ?? null : null;
|
|
1866
|
+
const tSegId =
|
|
1867
|
+
ci && typeof ci.target_segment_id === "number"
|
|
1868
|
+
? ci.target_segment_id
|
|
1869
|
+
: -1;
|
|
1870
|
+
const targetRoom =
|
|
1871
|
+
tSegId >= 0 ? this.segmentRoomNames?.[duid]?.[tSegId] ?? null : null;
|
|
1872
|
+
const state = typeof raw.state === "number" ? raw.state : null;
|
|
1873
|
+
const inCleaning =
|
|
1874
|
+
typeof raw.inCleaning === "number" ? raw.inCleaning : null;
|
|
1875
|
+
|
|
1876
|
+
// target_segment_id and in_cleaning MUST be in the dedupe key: they change
|
|
1877
|
+
// while segment_id/state may not, so without them those transitions (e.g. a
|
|
1878
|
+
// target flip -1 -> N -> -1) would be deduped away.
|
|
1879
|
+
const key = `${segId}|${room}|${state}|${tSegId}|${targetRoom}|${inCleaning}`;
|
|
1880
|
+
if (key === this._lastRoomPublishKey[duid]) return; // dedupe (ts excluded)
|
|
1881
|
+
this._lastRoomPublishKey[duid] = key;
|
|
1882
|
+
|
|
1883
|
+
const topic = this.resolveRoomTopic(duid);
|
|
1884
|
+
const payload = JSON.stringify({
|
|
1885
|
+
segment_id: segId,
|
|
1886
|
+
room,
|
|
1887
|
+
state,
|
|
1888
|
+
target_segment_id: tSegId,
|
|
1889
|
+
target_room: targetRoom,
|
|
1890
|
+
in_cleaning: inCleaning,
|
|
1891
|
+
ts: Date.now(),
|
|
1892
|
+
});
|
|
1893
|
+
client.publish(topic, payload, { retain: true, qos: 0 }, (err) => {
|
|
1894
|
+
if (err) {
|
|
1895
|
+
this.log.debug(`current_room MQTT publish failed: ${err.message}`);
|
|
1896
|
+
this._lastRoomPublishKey[duid] = undefined; // allow retry on next change
|
|
1897
|
+
}
|
|
1898
|
+
});
|
|
1899
|
+
} catch (e) {
|
|
1900
|
+
this.log.debug(`publishCurrentRoom error: ${e && e.message}`);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1767
1904
|
async getRobotVersion(duid) {
|
|
1768
1905
|
const homedata = await this.getStateAsync("HomeData");
|
|
1769
1906
|
if (homedata && homedata.val) {
|
|
@@ -1915,6 +2052,45 @@ class Roborock {
|
|
|
1915
2052
|
return !until || Date.now() >= until;
|
|
1916
2053
|
}
|
|
1917
2054
|
|
|
2055
|
+
// Called by the message queue when a LOCAL request times out. After a few
|
|
2056
|
+
// consecutive local timeouts we treat the device's local connection as
|
|
2057
|
+
// unresponsive (connected but mute) and prefer the cloud path for a cooldown.
|
|
2058
|
+
recordLocalTimeout(duid) {
|
|
2059
|
+
if (!duid) {
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
const streak = (this.localTimeoutStreak.get(duid) || 0) + 1;
|
|
2064
|
+
|
|
2065
|
+
if (streak >= this.localFailoverThreshold) {
|
|
2066
|
+
this.localCloudPreference.set(duid, Date.now() + this.localFailoverInterval);
|
|
2067
|
+
this.localTimeoutStreak.set(duid, 0);
|
|
2068
|
+
this.log.info(
|
|
2069
|
+
`Local connection for ${duid} is unresponsive after ${streak} consecutive timeouts; preferring cloud for ${Math.round(this.localFailoverInterval / 60000)} min.`
|
|
2070
|
+
);
|
|
2071
|
+
} else {
|
|
2072
|
+
this.localTimeoutStreak.set(duid, streak);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// Called by the message queue when a LOCAL request succeeds. Clears any local
|
|
2077
|
+
// failover state so a recovered local connection is used again.
|
|
2078
|
+
recordLocalSuccess(duid) {
|
|
2079
|
+
if (!duid) {
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
this.localTimeoutStreak.delete(duid);
|
|
2084
|
+
this.localCloudPreference.delete(duid);
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
// Whether requests for a device should prefer the cloud path because its local
|
|
2088
|
+
// connection is connected but unresponsive (true during the failover cooldown).
|
|
2089
|
+
shouldPreferCloudLocally(duid) {
|
|
2090
|
+
const until = this.localCloudPreference.get(duid);
|
|
2091
|
+
return !!until && Date.now() < until;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
1918
2094
|
// Poll a method that a device may not support. A device that never answers
|
|
1919
2095
|
// (e.g. a newer-protocol model lacking the V1 method) backs off instead of
|
|
1920
2096
|
// timing out every cycle. See recordMethodTimeout/backoff.
|