Skip to content

feat(sensor): audit base_status fields, add station/consumable diagnostics#52

Open
jgus wants to merge 2 commits into
sjmotew:masterfrom
jgus:feat/base-status-sensors
Open

feat(sensor): audit base_status fields, add station/consumable diagnostics#52
jgus wants to merge 2 commits into
sjmotew:masterfrom
jgus:feat/base-status-sensors

Conversation

@jgus

@jgus jgus commented Jun 15, 2026

Copy link
Copy Markdown

Summary

A full decode of the RobotBaseStatus proto turned up several mislabeled
fields
already being parsed into state, plus a number of useful signals the
robot broadcasts that weren't surfaced. This corrects the former and exposes the
latter as diagnostic entities.

Field corrections (were mapped to the wrong proto field)

  • battery_healthcuringAgentConsumptionPercent (f38)
  • session_idbindedUuid (f13)
  • timestampstationBagHealthResetTime (f36)
  • upgrade status now reads f2 (not f4 = stage); download state reads f3 (not f1 = type)
  • BaseStatusField / UpgradeStatusField enums corrected to match

New entities (all from fields already broadcast)

  • Sensors: dust bag health (f35), detergent remaining (f41)
  • Binary sensors (problem device class, gated on the field being present):
    error/fault (f1 — exposes code/level/detail + a best-effort help-center link),
    clean-water tank, sewage tank, dust box, dust bag, station dust bag

Binary sensors are now description-driven (small refactor) to keep the additions
declarative.

Caveat

The error help-center URL is a best-effort deep link — the app builds it from
a localized base we can't read over LAN, so the template is inferred from the
Flow's help-center family and falls back to showing the raw code. Worth a sanity
check if a real fault opens the wrong page.

How it was derived

Field/enum identities decoded from the app's compiled protobuf BuilderInfo and
constant pool; live-confirmed where possible.

Testing

New tests/test_binary_sensor.py; test_models.py/ha_stubs updated; suite green.

Relates to


Dependent PRs, stacked on this one (review after it merges): #53 (last clean result), #54 (consumable alerts).

…gnostics

Decoded the full RobotBaseStatus proto and corrected mislabeled fields
parsed into state: battery_health→curingAgentConsumptionPercent (f38),
session_id→bindedUuid (f13), timestamp→stationBagHealthResetTime (f36),
upgrade status (read f2, not f4=stage), download state (read f3, not
f1=type); BaseStatusField/UpgradeStatusField enums corrected to match.

Added diagnostics from fields the robot already broadcasts:
  - sensors: dust bag health (f35), detergent remaining (f41)
  - binary_sensors (problem, field-gated): error/fault (f1, exposing
    code/level/detail + a best-effort help-center link), clean-water and
    sewage tanks, dust box, dust bag, station dust bag

Binary sensors are now description-driven; ha_stubs gains the
BinarySensorEntityDescription stub for the new tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 15, 2026

Copy link
Copy Markdown

Greptile Summary

This PR corrects several mislabeled proto field mappings in NarwalState (battery_health→curingAgentConsumptionPercent, session_id→bindedUuid, timestamp→stationBagHealthResetTime, upgrade/download field numbers) and exposes station/consumable diagnostics that were already broadcast by the robot but never surfaced in HA.

  • Field corrections rename the affected state attributes and update BaseStatusField/UpgradeStatusField enums to match the decompiled proto BuilderInfo; tests and translations are updated throughout.
  • New binary sensors (error + five tank/bag PROBLEM sensors) use a clean description-driven pattern; _parse_error_codes now unconditionally resets fault state when field 1 is absent, correctly handling the standard proto encoding of an empty repeated field.
  • New sensors for dust_bag_health and detergent_remaining gate availability on raw_base_status key presence, which works correctly as long as those fields appear in every base_status payload (broadcast and polled).

Confidence Score: 5/5

Safe to merge; field corrections are backed by decompiled proto evidence, the stale-fault regression case is explicitly tested, and new entities degrade gracefully to unavailable on models that do not report those fields.

The field-rename corrections are consistently applied across both the standalone narwal_client tree and the custom_components mirror. The error-clear-on-absent-field fix is the most behaviorally sensitive change and is covered by a dedicated regression test. Both findings are design or style concerns that do not affect correctness under the stated assumption that station-equipped models always include fields 35 and 41 in every base_status response.

custom_components/narwal/sensor.py — the dust_bag_health and detergent_remaining value functions gate availability on the most-recent raw_base_status snapshot; worth verifying with a live deep-sleep capture that fields 35 and 41 are present in polled responses.

Important Files Changed

Filename Overview
narwal_client/models.py Core model changes: renames mislabeled state fields, extracts _update_consumables/_parse_error_codes helpers, adds error/tank/bag state tracking; error-clear-on-absent-field is handled correctly.
custom_components/narwal/binary_sensor.py Adds description-driven NarwalBinarySensor with error/tank/bag problem sensors; _tank_problem factory correctly uses None-based unavailability; attrs_fn for the error entity returns None when unavailable.
custom_components/narwal/sensor.py Adds dust_bag_health and detergent_remaining sensors; both gate availability on raw_base_status key presence, which may blink unavailable if any payload omits those keys.
custom_components/narwal/narwal_client/const.py BaseStatusField and UpgradeStatusField enums corrected from live proto decode; fields 20, 21, and 39 used in models but absent from the enum.
tests/test_binary_sensor.py New test file covering error-gate, error-clear-on-absent-field, multi-code parsing, tank/bag unavailability, and UNSPECIFIED-is-not-a-problem.
tests/test_models.py Updated to match renamed fields; adds tests for consumables, error codes, tank states, and corrected upgrade/download field numbers.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[base_status received] --> B{Source?}
    B -->|Broadcast| C[update_from_base_status]
    B -->|Deep-sleep poll| D[update_battery_from_base_status]
    C --> E[raw_base_status = decoded]
    D --> F[raw_base_status = decoded]
    E --> G[field 2 battery_level]
    E --> H[_update_consumables]
    F --> G2[field 2 battery_level]
    F --> H2[_update_consumables]
    H --> N[_parse_error_codes field 1]
    H --> O[fields 20 21 23 24 39 tank states]
    H --> J[field 35 dust_bag_health]
    H --> K[field 41 detergent_remaining]
    N --> P{field 1 absent?}
    P -->|Yes| Q[error_codes empty has_error False]
    P -->|No| R[parse codes level detail]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[base_status received] --> B{Source?}
    B -->|Broadcast| C[update_from_base_status]
    B -->|Deep-sleep poll| D[update_battery_from_base_status]
    C --> E[raw_base_status = decoded]
    D --> F[raw_base_status = decoded]
    E --> G[field 2 battery_level]
    E --> H[_update_consumables]
    F --> G2[field 2 battery_level]
    F --> H2[_update_consumables]
    H --> N[_parse_error_codes field 1]
    H --> O[fields 20 21 23 24 39 tank states]
    H --> J[field 35 dust_bag_health]
    H --> K[field 41 detergent_remaining]
    N --> P{field 1 absent?}
    P -->|Yes| Q[error_codes empty has_error False]
    P -->|No| R[parse codes level detail]
Loading

Reviews (2): Last reviewed commit: "fix(base-status): clear error state when..." | Re-trigger Greptile

Comment thread narwal_client/models.py
Comment thread narwal_client/models.py
protobuf omits an empty repeated field, so a recovered robot drops the
errorCode field entirely. The old `if "1" in decoded` guard left has_error
and error_codes latched on the prior fault forever, sticking the Error
sensor on "Problem". Parse unconditionally via decoded.get("1") — the parser
already clears to no-error on None/empty.

Also corrects the update_battery_from_base_status docstring (it refreshes
consumable/station/fault/tank state too, not battery alone) and adds a
regression test for the field-absent recovery path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant