homebridge-roborock-matter 3.8.0 → 3.9.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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.9.0
4
+
5
+ **Automations can now be triggered by the robot.**
6
+
7
+ Apple Home does not accept a Matter vacuum as an automation trigger at all. pponce measured that in [#3](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/3) and confirmed it again after the action switches shipped: the switches are commands an automation _sends_, so nothing the robot does can _start_ one. A contact sensor is a trigger source in every Home client, so each robot can now publish read-only sensors mirroring its state — `Vicky Docked` and `Vicky Cleaning`.
8
+
9
+ Two states, in the order he ranked them when asked which he would actually trigger on: docked first ("I'd use the docked feature on its own for sure"), cleaning second. He also named the pair he wants them for — not docked **and** not cleaning means the robot is probably stuck somewhere — which is why both ship together, and why there is no third "stuck" sensor: that one is a timeout over these two, and the timeout is his to pick.
10
+
11
+ Closed means the state the sensor is named after is true, in every sensor. Nothing is ever sent to the robot.
12
+
13
+ Three things the implementation is careful about, each enumerated as a rule rather than fixed for the case that prompted it:
14
+
15
+ - **The value comes from the robot's own state, never from the state Apple Home was told.** Two unrelated display toggles rewrite the published operational state: CHARGING and DOCKED become STOPPED without **Charging/Docked status**, and the dock chores become RUNNING without **Dock & Returning status**. A `Docked` sensor built on the published value would have worked only for users who had ticked a box about something else — the same fault form as [#9](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/9)'s fix and [#5](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/5)'s. Verified red against that exact wrong implementation: 13 assertions failed, on the docked-robot and dock-chore rows specifically.
16
+ - **A dock chore is not a cleaning run, and interrupting a run does not end it.** `Cleaning` mirrors the run mode that actually reached Matter, so it carries 3.6.2's inheritance rule and cannot disagree with the tile. The test asserts that agreement as an identity rather than as a second hand-written truth table, plus a guard that both values genuinely occur.
17
+ - **Nothing is claimed before the robot has reported in.** Roborock state 0 is not a real state; it maps to STOPPED, which is indistinguishable from a robot idle on the floor. A Q7 on the maintainer's own account reports it for 27 seconds after every restart, so a sensor that believed it would report "not docked" for a docked robot and then move — firing every automation watching for that, on every Homebridge restart. The sensors hold their last known reading and only move on real data. Verified red with the guard removed.
18
+
19
+ The partition that keeps both HAP accessory kinds alive is the fourth: `discoverDevices()` unregisters cached HAP accessories it does not recognise, and each kind's sync removes what its own config no longer asks for. A sensor has no `action` in its context, so before this the switch sync would have deleted every sensor on the first discovery pass. Both directions are asserted, and verified red against the unpartitioned version.
20
+
21
+ Off by default, and no re-pairing: like the switches these are HAP accessories on this plugin's child bridge, not Matter. They need that bridge's own HomeKit QR code — the plugin says which one at every start.
22
+
23
+ 922 tests, up from 907.
24
+
3
25
  ## 3.8.0
4
26
 
5
27
  **The settings page follows Homebridge's dark theme, and the icon is in the header.**
package/README.md CHANGED
@@ -37,22 +37,23 @@ This is the most feature-packed, most thoroughly engineered Roborock plugin for
37
37
  - 📍 **See where it's cleaning — live.** Apple Home shows _"Cleaning — Kitchen"_ with the room the robot is actually inside, updating as it moves from room to room. Works even for cleans started from the robot's button or the Roborock app. No other Homebridge plugin does this.
38
38
  - 🧭 **One robot, one tile — and as many robots as you own.** Sign in once and your whole fleet comes along: every vacuum on your account appears as its own clean, native accessory in Apple Home. No clutter of fake fans and helper switches, and rooms appear with the names you gave them in the Roborock app.
39
39
  - ⚡ **Fast and reliable.** Commands go directly to the robot over your own network whenever possible, with the Roborock cloud as automatic backup — and built-in diagnostics in the settings if you ever want to look under the hood.
40
- - 🛡️ **Verified by Homebridge.** Reviewed and endorsed by the Homebridge team. 773 automated tests, zero known vulnerabilities, no analytics, and a startup designed to never crash your Homebridge — even when your Wi-Fi or the Roborock cloud has a bad day.
40
+ - 🛡️ **Verified by Homebridge.** Reviewed and endorsed by the Homebridge team. 922 automated tests, zero known vulnerabilities, no analytics, and a startup designed to never crash your Homebridge — even when your Wi-Fi or the Roborock cloud has a bad day.
41
41
 
42
42
  ## Features
43
43
 
44
- | | |
45
- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
46
- | 🤖 **Full control from Apple Home** | Start, stop, pause and send the robot home to its dock — from the Home app or Siri |
47
- | 🕹️ **Switches for automations** | Optional per-robot Start Cleaning, Return to Dock, Pause and Find switches — Apple Home does not offer a dock action for a Matter vacuum ([details](#automations-in-apple-home)) |
48
- | 🚪 **Clean specific rooms** | Pick rooms right in Apple Home, with the names you gave them in the Roborock app — multi-floor homes included |
49
- | 📍 **Live room tracking** | See which room the robot is cleaning right now, updated as it moves ([details](#live-room-tracking)) |
50
- | 📊 **Honest cleaning progress** | Each room goes pending → cleaning → done — and a room only counts as done when the robot was actually there |
51
- | 🌀 **Cleaning & suction modes** | Vacuum / Mop / Vacuum + Mop on models that support it — plus optional Quiet / Balanced / Turbo / Max suction levels (Max+ on Q7) |
52
- | 🔋 **Battery & charging** | Battery level and charging state on the accessory ([one Apple-side caveat](#battery-percentage-in-apple-home)) |
53
- | 🧠 **New models just work** | Brand-new Roborock models get sensible defaults automatically, and the plugin adapts to what each robot actually supports |
54
- | 🩺 **Built-in diagnostics** | Connection status, a one-click connection test, and a ready-to-share report if you ever need help |
55
- | 🔐 **Easy, safe login** | Sign in with your Roborock account right in the settings — two-factor supported, session stored encrypted |
44
+ | | |
45
+ | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
46
+ | 🤖 **Full control from Apple Home** | Start, stop, pause and send the robot home to its dock — from the Home app or Siri |
47
+ | 🕹️ **Switches for automations** | Optional per-robot Start Cleaning, Return to Dock, Pause and Find switches — Apple Home does not offer a dock action for a Matter vacuum ([details](#automations-in-apple-home)) |
48
+ | 🎯 **Sensors as automation triggers** | Optional per-robot Docked and Cleaning contact sensors — a Matter vacuum is not offered as an automation trigger at all, and a contact sensor is ([details](#automations-in-apple-home)) |
49
+ | 🚪 **Clean specific rooms** | Pick rooms right in Apple Home, with the names you gave them in the Roborock app — multi-floor homes included |
50
+ | 📍 **Live room tracking** | See which room the robot is cleaning right now, updated as it moves ([details](#live-room-tracking)) |
51
+ | 📊 **Honest cleaning progress** | Each room goes pending → cleaning → done — and a room only counts as done when the robot was actually there |
52
+ | 🌀 **Cleaning & suction modes** | Vacuum / Mop / Vacuum + Mop on models that support it — plus optional Quiet / Balanced / Turbo / Max suction levels (Max+ on Q7) |
53
+ | 🔋 **Battery & charging** | Battery level and charging state on the accessory ([one Apple-side caveat](#battery-percentage-in-apple-home)) |
54
+ | 🧠 **New models just work** | Brand-new Roborock models get sensible defaults automatically, and the plugin adapts to what each robot actually supports |
55
+ | 🩺 **Built-in diagnostics** | Connection status, a one-click connection test, and a ready-to-share report if you ever need help |
56
+ | 🔐 **Easy, safe login** | Sign in with your Roborock account right in the settings — two-factor supported, session stored encrypted |
56
57
 
57
58
  ## Quick start
58
59
 
@@ -103,20 +104,22 @@ The clean mode follows the robot as well: start a vacuum+mop or mop-only clean f
103
104
 
104
105
  Everything is configurable from the Homebridge UI. The essentials:
105
106
 
106
- | Option | Default | What it does |
107
- | ------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
108
- | `email` / password | — | Your Roborock app account (2FA handled in the UI; the session token is stored encrypted) |
109
- | `skipDevices` | — | Comma-separated device IDs the plugin should ignore |
110
- | `enableMatterServiceArea` | `true` | Room/map selection in Apple Home |
111
- | `enableLiveRoomTracking` | `true` | Live current-room from the robot's map position while cleaning |
112
- | `enableMatterCleanMode` | `true` | Vacuum / Mop / Vacuum + Mop mode selection |
113
- | `enableFanPowerCleanModes` | `false` | Adds Quiet / Balanced / Turbo / Max (and Max+ on Q7) suction modes to the Matter mode list. **Re-pair the robot once after toggling** — Matter locks the mode list at pairing |
114
- | `enableMatterPowerSource` | `true` | Battery cluster |
115
- | `enableMatterFaultReporting` | `false` | Report a robot that has genuinely halted as Error instead of Ready ([details](#why-the-robot-needs-attention)) |
116
- | `enableHomeKitActionSwitches` | `false` | Adds a plain Home app switch per robot for Start Cleaning / Return to Dock / Pause / Find, so automations can reach commands Apple does not offer for a Matter vacuum ([details](#automations-in-apple-home)) |
117
- | `homeKitActionSwitches` | `["dock"]` | Which of those switches to publish: `clean`, `dock`, `pause`, `locate` |
118
- | `cloudOnlyMode` | `false` | Skip local TCP entirely and use the cloud for everything |
119
- | `transientWarningThrottleHours` | `6` | How often recurring transient-timeout warnings may repeat (0 = only in debug) |
107
+ | Option | Default | What it does |
108
+ | ------------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
109
+ | `email` / password | — | Your Roborock app account (2FA handled in the UI; the session token is stored encrypted) |
110
+ | `skipDevices` | — | Comma-separated device IDs the plugin should ignore |
111
+ | `enableMatterServiceArea` | `true` | Room/map selection in Apple Home |
112
+ | `enableLiveRoomTracking` | `true` | Live current-room from the robot's map position while cleaning |
113
+ | `enableMatterCleanMode` | `true` | Vacuum / Mop / Vacuum + Mop mode selection |
114
+ | `enableFanPowerCleanModes` | `false` | Adds Quiet / Balanced / Turbo / Max (and Max+ on Q7) suction modes to the Matter mode list. **Re-pair the robot once after toggling** — Matter locks the mode list at pairing |
115
+ | `enableMatterPowerSource` | `true` | Battery cluster |
116
+ | `enableMatterFaultReporting` | `false` | Report a robot that has genuinely halted as Error instead of Ready ([details](#why-the-robot-needs-attention)) |
117
+ | `enableHomeKitActionSwitches` | `false` | Adds a plain Home app switch per robot for Start Cleaning / Return to Dock / Pause / Find, so automations can reach commands Apple does not offer for a Matter vacuum ([details](#automations-in-apple-home)) |
118
+ | `homeKitActionSwitches` | `["dock"]` | Which of those switches to publish: `clean`, `dock`, `pause`, `locate` |
119
+ | `enableHomeKitStateSensors` | `false` | Adds a read-only Home app contact sensor per robot mirroring its state, so an automation can be _triggered_ by the robot — a Matter vacuum is not offered as a trigger, and a contact sensor is ([details](#automations-in-apple-home)) |
120
+ | `homeKitStateSensors` | `["docked"]` | Which of those sensors to publish: `docked`, `cleaning` |
121
+ | `cloudOnlyMode` | `false` | Skip local TCP entirely and use the cloud for everything |
122
+ | `transientWarningThrottleHours` | `6` | How often recurring transient-timeout warnings may repeat (0 = only in debug) |
120
123
 
121
124
  ## Why the robot needs attention
122
125
 
@@ -136,7 +139,7 @@ What Apple offers _inside_ Home automations is a separate question, and it is Ap
136
139
 
137
140
  - **Starting a clean is offered as an automation action** — either the whole home or a chosen set of rooms — and so is **stopping** or **pausing** a clean that is already running. An Apple Home schedule can therefore do the things most schedules are built for, without any help from this section.
138
141
  - **Sending the vacuum to its dock is not offered as an automation action.** A robot that finishes a clean normally returns to its dock by itself, so the gap only shows up when you want to end a clean early: the automation can cut it short, but it cannot call the robot home.
139
- - **A Matter vacuum is not offered as an automation _trigger_ at all.** The vacuum could not be selected when setting an automation's trigger — only when choosing its action. "When the robot finishes cleaning, close the balcony door" is therefore not expressible in Apple Home today, and the switches below do not change it: they are inputs an automation can turn on, not accessories that report what the robot is doing.
142
+ - **A Matter vacuum is not offered as an automation _trigger_ at all.** The vacuum could not be selected when setting an automation's trigger — only when choosing its action. The switches below do not change that: they are inputs an automation can turn on, not accessories that report what the robot is doing. The **sensors** below do, which is why they exist.
140
143
  - **Whether an automation can resume a paused clean has not been measured.** Nobody has looked, so this page claims nothing about it in either direction.
141
144
 
142
145
  **Optional Home app switches close the docking gap.** Turn on **Add Home app switches for Start, Dock, Pause and Find** in the plugin settings and each robot gets one plain HomeKit switch per action you pick — `Vicky Start Cleaning`, `Vicky Return to Dock`, `Vicky Pause`, `Vicky Find`. A switch is something every automation, scene and Shortcut can turn on, which is the whole point: an automation that cannot send the robot to its dock directly can flip a switch that does it instead. Each one is momentary and turns itself off again about a second and a half after it is pressed, so it never claims a command is still running.
@@ -145,17 +148,29 @@ What Apple offers _inside_ Home automations is a separate question, and it is Ap
145
148
 
146
149
  A press takes exactly the same route as a press on the tile — the same acknowledgement wait, the same timing line in the log, the same retry if Roborock times out while the robot is still cleaning — and it moves the tile with it, so a robot sent home by a schedule does not sit there reading Ready. The log line names which surface asked, so `Sending Vicky back to dock from the Home switch.` and `Sending Vicky back to dock from Matter.` are told apart when a schedule misfires.
147
150
 
148
- ### The switches need their own pairing — a different QR code
151
+ **Optional Home app sensors close the trigger gap.** A Matter vacuum is not offered as an automation trigger, but a HomeKit contact sensor is — so turn on **Add Home app sensors so automations can trigger on the robot** and each robot gets one read-only contact sensor per state you pick: `Vicky Docked` and `Vicky Cleaning`. That makes "when the robot leaves its dock, do X" expressible. Each one reads **Closed** while the state it is named after is true and **Open** when it is not: `Vicky Docked` is Closed in the dock and Open once the robot drives off. Nothing is ever sent to the robot — these only report.
149
152
 
150
- This is the one step that quietly produces "I turned it on and nothing appeared", so it is worth reading before you do anything else. Your robot reaches Apple Home over **Matter**. These switches are ordinary **HomeKit** accessories, and they travel on this plugin's own Homebridge child bridge, which Apple Home pairs **separately**. The code you scanned for the vacuum does not cover them.
153
+ The pair is more useful than either alone, and that is the automation they were asked for in [#3](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/3): **not docked and not cleaning means the robot is probably stuck somewhere.** A robot that has genuinely halted reads Open on both.
154
+
155
+ Three details that are deliberate:
156
+
157
+ - **A dock chore is not a cleaning run.** Emptying the dust bin, washing the mop and updating maps leave `Cleaning` exactly where it was — Open if the robot was resting, Closed if it was interrupted mid-run. This is the same rule the Home tile follows since 3.6.2, and the sensor is checked against the tile rather than against a second opinion, so the two can never disagree.
158
+ - **`Docked` does not depend on any other setting.** It reads the robot's own charging state, not the state Apple Home was told, so it works the same whether or not **Dock & Returning status** and **Charging/Docked status** are switched on. `Cleaning` mirrors the tile, so with **Dock & Returning status** off it goes Open when the robot starts driving home rather than when it arrives — that is what the tile says too.
159
+ - **Nothing is claimed before the robot has reported in.** For the first seconds after a Homebridge restart some robots report no usable state at all, and a sensor that guessed would move once the real value arrived — firing every automation watching for it, on every restart. The sensors hold their last known reading instead and only move on real data.
160
+
161
+ They are off by default, because switching them on adds accessories to your Home app.
162
+
163
+ ### The switches and sensors need their own pairing — a different QR code
164
+
165
+ This is the one step that quietly produces "I turned it on and nothing appeared", so it is worth reading before you do anything else. Your robot reaches Apple Home over **Matter**. These switches and sensors are ordinary **HomeKit** accessories, and they travel on this plugin's own Homebridge child bridge, which Apple Home pairs **separately**. The code you scanned for the vacuum does not cover them.
151
166
 
152
167
  In the Homebridge UI, go to **Plugins → homebridge-roborock-matter → ⋮ → Child Bridge Config**, and then:
153
168
 
154
- 1. Check that **Enable HAP** is on. On a Matter-only setup it is frequently off — and while it is off, the switches exist inside Homebridge but are not published to anything, so no QR code anywhere will bring them in.
169
+ 1. Check that **Enable HAP** is on. On a Matter-only setup it is frequently off — and while it is off, they exist inside Homebridge but are not published to anything, so no QR code anywhere will bring them in.
155
170
  2. Save and restart Homebridge.
156
171
  3. Return to the same screen and press **Connect to HomeKit**. That is the QR code to scan in the Home app.
157
172
 
158
- It is **not** the main Homebridge QR code on the status page, and **not** the robot's Matter pairing code. The plugin tells you which of those three situations you are in: every start it writes one line naming the bridge the switches went to and what, if anything, is still missing.
173
+ It is **not** the main Homebridge QR code on the status page, and **not** the robot's Matter pairing code. The plugin tells you which of those three situations you are in: every start it writes one line naming the bridge they went to and what, if anything, is still missing.
159
174
 
160
175
  Two smaller things. They are off by default because switching them on adds accessories to your Home app, one per robot per action. And the Find switch is only published for robots that actually support the command, because a switch that silently does nothing is worse than no switch at all.
161
176
 
@@ -182,7 +197,7 @@ The complete path — robot → plugin → Homebridge → matter.js store — wa
182
197
 
183
198
  ## Contributing
184
199
 
185
- Model reports, diagnostics exports, and pull requests are very welcome. The codebase ships with 773 tests (protocol fixtures verified against the [python-roborock](https://github.com/Python-roborock/python-roborock) reference), strict TypeScript checking, and CI across Node 22/24 × Homebridge 1.11/2.x — `npm test` before you push and you're set.
200
+ Model reports, diagnostics exports, and pull requests are very welcome. The codebase ships with 922 tests (protocol fixtures verified against the [python-roborock](https://github.com/Python-roborock/python-roborock) reference), strict TypeScript checking, and CI across Node 22/24 × Homebridge 1.11/2.x — `npm test` before you push and you're set.
186
201
 
187
202
  ## Support the project
188
203
 
@@ -159,6 +159,33 @@
159
159
  ]
160
160
  }
161
161
  },
162
+ "enableHomeKitStateSensors": {
163
+ "title": "Add Home App Sensors so Automations Can Trigger on the Robot",
164
+ "description": "Publishes one extra read-only HomeKit contact sensor per robot per state, so an Apple Home automation can START when the robot does something. The switches above are commands an automation sends; Apple Home will not accept a Matter vacuum as an automation TRIGGER at all, which was measured twice in issue #3, so nothing the robot does can begin an automation. A contact sensor is a trigger source in every Home client. Each sensor reads Closed while the state it is named after is true and Open when it is not — \"Docked\" is Closed in the dock and Open once the robot leaves. Off by default, because turning it on adds accessories to your Home app. IMPORTANT — THESE NEED THE SAME PAIRING AS THE SWITCHES ABOVE: they are HomeKit accessories on this plugin's Homebridge child bridge, which is paired separately from the robot's Matter connection. Go to Plugins -> homebridge-roborock-matter -> the three-dot menu -> Child Bridge Config, make sure Enable HAP is ON, restart, then press Connect to HomeKit on that same screen and scan THAT QR code. It is not the main Homebridge QR code, and not the robot's Matter pairing code.",
165
+ "type": "boolean",
166
+ "default": false
167
+ },
168
+ "homeKitStateSensors": {
169
+ "title": "Which Sensors to Add",
170
+ "description": "Which states get a sensor. Docked is the one to start with: it is the state most automations read, and \"not docked\" is its own answer. Cleaning is the pair for it — not docked AND not cleaning means the robot is probably stuck somewhere, which is the automation this was asked for. Only used when the setting above is on.",
171
+ "type": "array",
172
+ "uniqueItems": true,
173
+ "default": ["docked"],
174
+ "items": {
175
+ "type": "string",
176
+ "enum": ["docked", "cleaning"],
177
+ "oneOf": [
178
+ {
179
+ "title": "Docked (Closed while the robot is in its dock)",
180
+ "enum": ["docked"]
181
+ },
182
+ {
183
+ "title": "Cleaning (Closed while the robot is on a cleaning run)",
184
+ "enum": ["cleaning"]
185
+ }
186
+ ]
187
+ }
188
+ },
162
189
  "cloudOnlyMode": {
163
190
  "title": "Use Roborock Cloud Only",
164
191
  "description": "Disables local LAN discovery and local TCP commands for this plugin, routing commands and status polling through Roborock cloud when available. Useful when local LAN connections appear connected but consistently time out. Restart the Roborock child bridge after changing this setting.",
@@ -280,6 +280,13 @@ class RoborockMatterVacuumAccessory {
280
280
  // resolveRunMode(). Idle is the honest starting point: a plugin that boots
281
281
  // while the dock is emptying knows of no run in progress.
282
282
  this.lastRunMode = RUN_MODE_IDLE;
283
+ // The run mode as it went out to Matter, optimistic overlay included. The
284
+ // read-only "Cleaning" state sensor answers from this so it and the Apple
285
+ // Home tile always say the same thing. Null until the first publish.
286
+ this.lastPublishedRunMode = null;
287
+ // Notified after every publish, by whoever wants to mirror this robot's state
288
+ // somewhere else. Null means nobody asked, which is the common case.
289
+ this.stateListener = null;
283
290
  // The suction-level clean mode last derived from a fan power the plugin
284
291
  // could actually read. Used only as the answer to "the fan power is
285
292
  // unreadable right now" while suction levels are announced; cleared by an
@@ -368,6 +375,72 @@ class RoborockMatterVacuumAccessory {
368
375
  }
369
376
  return true;
370
377
  }
378
+ /**
379
+ * The value a read-only HAP state sensor should show, or null for "not yet".
380
+ *
381
+ * Both arms read the ROBOT'S OWN state, never the controller-facing one that
382
+ * toControllerOperationalState() produces. That is not a stylistic choice: it
383
+ * is the fault form this file has now been bitten by seven times. CHARGING
384
+ * and DOCKED are rewritten to STOPPED unless the user enabled the
385
+ * charging/docked toggle, and the dock chores are rewritten to RUNNING unless
386
+ * they enabled the extended-states one — so a docked sensor built on the
387
+ * published operational state would have worked only for the users who had
388
+ * ticked an unrelated box, and reported "not docked" for everybody else.
389
+ *
390
+ * `cleaning` mirrors the run mode that was last PUBLISHED rather than
391
+ * recomputing one, for three reasons. It is the value Apple Home was actually
392
+ * told, so the sensor and the tile cannot disagree — including during the
393
+ * optimistic window after a command, where the tile moves before the robot
394
+ * confirms and a sensor computed from raw status would lag it by a poll. It
395
+ * carries 3.6.2's rule that a dock chore inherits the run mode it interrupted,
396
+ * so emptying the dust bin does not make the sensor announce a cleaning that
397
+ * is not happening — the exact bug issue #9 reported against the tile, which
398
+ * would otherwise have been reintroduced one surface over. And resolveRunMode()
399
+ * is deliberately NOT called here: it assigns lastRunMode, and a getter a HAP
400
+ * read can reach must not advance the state machine that decides what gets
401
+ * published.
402
+ */
403
+ /** Ask to be told after every publish. Null clears it. */
404
+ setStateListener(listener) {
405
+ this.stateListener = listener;
406
+ }
407
+ getHomeKitStateSensorValue(sensor) {
408
+ var _a;
409
+ if (!this.hasUsableRobotState()) {
410
+ return null;
411
+ }
412
+ switch (sensor) {
413
+ case "docked":
414
+ return this.isDockedOrChargingNow();
415
+ case "cleaning":
416
+ return (((_a = this.lastPublishedRunMode) !== null && _a !== void 0 ? _a : this.lastRunMode) === RUN_MODE_CLEANING);
417
+ default:
418
+ return null;
419
+ }
420
+ }
421
+ /**
422
+ * Whether the robot has reported enough for a sensor to claim anything.
423
+ *
424
+ * State 0 is not a Roborock state — the enum starts at 1 and the mapping
425
+ * switch has no arm for it, so it falls to the default branch and comes out
426
+ * as STOPPED. That is indistinguishable from a robot that is genuinely idle
427
+ * off its dock, which is why this is checked here rather than left to the
428
+ * mapping: a Q7 on this account has been measured reporting state 0 for 27
429
+ * seconds after every restart, and a sensor that believed it would report
430
+ * "not docked" for a robot sitting in its dock, then flip — firing every
431
+ * automation triggered on the robot leaving, on every Homebridge restart.
432
+ *
433
+ * A non-zero charge_status is a complete answer on its own: the robot is on
434
+ * the dock drawing power whatever it says its state is.
435
+ */
436
+ hasUsableRobotState() {
437
+ const chargeStatus = this.getNumberStatus("charge_status");
438
+ if (chargeStatus !== null && chargeStatus !== 0) {
439
+ return true;
440
+ }
441
+ const state = this.getNumberStatus("state");
442
+ return state !== null && state !== 0;
443
+ }
371
444
  /**
372
445
  * Perform an action requested by one of the optional HAP switches.
373
446
  *
@@ -900,6 +973,14 @@ class RoborockMatterVacuumAccessory {
900
973
  // The full snapshot as built, kept across the diff below so the evidence
901
974
  // line always reports every value — not just the clusters that changed.
902
975
  const snapshot = clusters;
976
+ // Before the diff below, and before the early return it can take: this is
977
+ // the one place every Roborock-driven state change passes through, so it is
978
+ // the only hook that cannot miss one. The unchanged-payload path matters
979
+ // just as much as the changed one — a listener's first reading after a
980
+ // restart usually arrives on a poll whose clusters are byte-identical to
981
+ // what the previous process already published.
982
+ this.rememberPublishedRunMode(snapshot);
983
+ this.notifyStateListener();
903
984
  if (options.force !== true) {
904
985
  const changed = {};
905
986
  for (const [cluster, attributes] of Object.entries(clusters)) {
@@ -939,6 +1020,37 @@ class RoborockMatterVacuumAccessory {
939
1020
  }
940
1021
  return updated;
941
1022
  }
1023
+ /**
1024
+ * Tell whoever asked that this robot's published state may have moved.
1025
+ *
1026
+ * A listener rather than a call into the platform's sensor map, so this class
1027
+ * stays unaware that read-only HAP sensors exist at all. The first draft did
1028
+ * reach into the platform, and the cost showed up immediately: seventeen test
1029
+ * suites build their own platform stand-in, and every one of them would have
1030
+ * had to grow a method about a feature it was not testing — with the next
1031
+ * stand-in forgetting it again. Nothing else in this file needs the platform
1032
+ * to own that knowledge, so it does not.
1033
+ */
1034
+ notifyStateListener() {
1035
+ if (!this.stateListener) {
1036
+ return;
1037
+ }
1038
+ try {
1039
+ this.stateListener();
1040
+ }
1041
+ catch (error) {
1042
+ // A listener that throws must not take the Matter publish down with it.
1043
+ this.platform.log.debug(`State listener for ${this.getVacuumName()} failed: ${this.getErrorMessage(error)}`);
1044
+ }
1045
+ }
1046
+ /** Keep the run mode the state sensors answer from in step with Matter's. */
1047
+ rememberPublishedRunMode(snapshot) {
1048
+ const runMode = snapshot.rvcRunMode;
1049
+ const currentMode = runMode === null || runMode === void 0 ? void 0 : runMode.currentMode;
1050
+ if (typeof currentMode === "number") {
1051
+ this.lastPublishedRunMode = currentMode;
1052
+ }
1053
+ }
942
1054
  async updateMatterStateFromMessage(data) {
943
1055
  if (!this.registered) {
944
1056
  return;