diff --git a/custom_components/narwal/narwal_client/const.py b/custom_components/narwal/narwal_client/const.py index 0c877c7..168f5b2 100644 --- a/custom_components/narwal/narwal_client/const.py +++ b/custom_components/narwal/narwal_client/const.py @@ -227,20 +227,19 @@ class UpgradeStatusField(IntEnum): class WorkingStatusField(IntEnum): """Field numbers in the working_status protobuf message. - Confirmed via live test (2026-02-27): - 3 = current session elapsed seconds (confirmed: 2136→2159 over 35-min clean) - 13 = cleaning area in cm² (confirmed: 18000 = 1.8m²) - 15 = 600 during cleaning (possibly cumulative or constant) - 6 = 1 during cleaning (observed in plan-based clean; may vary by mode) - 10 = time since docked in seconds (post-dock only, counts up) - 11 = 2700 post-dock (unknown, constant) - - Also broadcast during cleaning: - status/time_line_status — timeline/history data - developer/planning_debug_info — navigation debug (collision count, stall count) + Names from the decompiled WorkingStatus proto BuilderInfo: + 1 = workingProgress (float32, PbFieldType 0x100) + 2 = coveredArea (float32, PbFieldType 0x100) — area cleaned this session, m² + 3 = timeConsuming (seconds) — session elapsed time + 4 = remainedTime (seconds) + 6 = cleaningZoneId + 8..17 = station drying/sterilization/dust-bag timers (seconds); the + cumulative "total*" counters (9/11/13/15/17) stay constant while + idle. Field 13 = totalDryStationBagTime (18000 = 5h) — earlier + misread as cleaning area because 18000/10000 looked like 1.8 m². """ - ELAPSED_TIME = 3 # current session elapsed seconds — CONFIRMED - AREA = 13 # cm² — CONFIRMED (18000 = 1.8m²) - CUMULATIVE_TIME = 15 # 600 during cleaning (purpose uncertain) - TIME_SINCE_DOCKED = 10 # seconds since docked (post-dock only) + PROGRESS = 1 # workingProgress (float32, 0..1) + AREA = 2 # coveredArea (float32) — m² + ELAPSED_TIME = 3 # timeConsuming — session elapsed seconds + REMAINING_TIME = 4 # remainedTime — seconds diff --git a/custom_components/narwal/narwal_client/models.py b/custom_components/narwal/narwal_client/models.py index 11a69c6..37376c5 100644 --- a/custom_components/narwal/narwal_client/models.py +++ b/custom_components/narwal/narwal_client/models.py @@ -509,7 +509,7 @@ class NarwalState: position: Position | None = None # Cleaning stats - cleaning_area: int = 0 # cm² + cleaning_area: float = 0.0 # m² (coveredArea) cleaning_time: int = 0 # seconds # Map @@ -627,11 +627,13 @@ def is_returning(self) -> bool: def update_from_working_status(self, decoded: dict[str, Any]) -> None: """Update state from a decoded working_status message. - Confirmed via 35-min monitor capture (2026-02-27): - Field 3 = current session elapsed time (seconds) - (confirmed: 2136→2159 over 35-min clean) - Field 13 = cleaning area (cm²) — CONFIRMED (18000 = 1.8m²) - Field 15 = 600 during cleaning (purpose uncertain) + WorkingStatus proto fields (decompiled BuilderInfo): + Field 2 = coveredArea (float32, PbFieldType 0x100) — area cleaned this session, m² + Field 3 = timeConsuming (seconds) — session elapsed time + (confirmed: 2136→2159 over a 35-min clean) + + Field 13 is totalDryStationBagTime (cumulative station timer, 18000 = 5h), + not area — reading it as area is why the sensor was stuck at 1.8 m². """ self.raw_working_status = decoded if "3" in decoded: @@ -639,11 +641,10 @@ def update_from_working_status(self, decoded: dict[str, Any]) -> None: self.cleaning_time = int(decoded["3"]) except (ValueError, TypeError): pass - if "13" in decoded: - self.cleaning_area = int(decoded["13"]) - if "15" in decoded: - # Field 15 may be cumulative time; prefer field 3 for current session - pass + if "2" in decoded: + area = _to_float32(decoded["2"]) + if area is not None and area >= 0: + self.cleaning_area = area def update_from_base_status(self, decoded: dict[str, Any]) -> None: """Update state from a decoded robot_base_status message. diff --git a/custom_components/narwal/sensor.py b/custom_components/narwal/sensor.py index 904b2fb..ffc3483 100644 --- a/custom_components/narwal/sensor.py +++ b/custom_components/narwal/sensor.py @@ -43,9 +43,8 @@ class NarwalSensorEntityDescription(SensorEntityDescription): translation_key="cleaning_area", native_unit_of_measurement=UnitOfArea.SQUARE_METERS, state_class=SensorStateClass.MEASUREMENT, - # working_status field 13 is cm²; divide by 10000 for m². - # NEEDS LIVE VALIDATION: only populated during active cleaning. - value_fn=lambda state: round(state.cleaning_area / 10000, 2) + # working_status field 2 (coveredArea) is already m²; populated only during active cleaning. + value_fn=lambda state: round(state.cleaning_area, 2) if state.cleaning_area > 0 else None, ), diff --git a/narwal_client/const.py b/narwal_client/const.py index 0c877c7..168f5b2 100644 --- a/narwal_client/const.py +++ b/narwal_client/const.py @@ -227,20 +227,19 @@ class UpgradeStatusField(IntEnum): class WorkingStatusField(IntEnum): """Field numbers in the working_status protobuf message. - Confirmed via live test (2026-02-27): - 3 = current session elapsed seconds (confirmed: 2136→2159 over 35-min clean) - 13 = cleaning area in cm² (confirmed: 18000 = 1.8m²) - 15 = 600 during cleaning (possibly cumulative or constant) - 6 = 1 during cleaning (observed in plan-based clean; may vary by mode) - 10 = time since docked in seconds (post-dock only, counts up) - 11 = 2700 post-dock (unknown, constant) - - Also broadcast during cleaning: - status/time_line_status — timeline/history data - developer/planning_debug_info — navigation debug (collision count, stall count) + Names from the decompiled WorkingStatus proto BuilderInfo: + 1 = workingProgress (float32, PbFieldType 0x100) + 2 = coveredArea (float32, PbFieldType 0x100) — area cleaned this session, m² + 3 = timeConsuming (seconds) — session elapsed time + 4 = remainedTime (seconds) + 6 = cleaningZoneId + 8..17 = station drying/sterilization/dust-bag timers (seconds); the + cumulative "total*" counters (9/11/13/15/17) stay constant while + idle. Field 13 = totalDryStationBagTime (18000 = 5h) — earlier + misread as cleaning area because 18000/10000 looked like 1.8 m². """ - ELAPSED_TIME = 3 # current session elapsed seconds — CONFIRMED - AREA = 13 # cm² — CONFIRMED (18000 = 1.8m²) - CUMULATIVE_TIME = 15 # 600 during cleaning (purpose uncertain) - TIME_SINCE_DOCKED = 10 # seconds since docked (post-dock only) + PROGRESS = 1 # workingProgress (float32, 0..1) + AREA = 2 # coveredArea (float32) — m² + ELAPSED_TIME = 3 # timeConsuming — session elapsed seconds + REMAINING_TIME = 4 # remainedTime — seconds diff --git a/narwal_client/models.py b/narwal_client/models.py index 11a69c6..37376c5 100644 --- a/narwal_client/models.py +++ b/narwal_client/models.py @@ -509,7 +509,7 @@ class NarwalState: position: Position | None = None # Cleaning stats - cleaning_area: int = 0 # cm² + cleaning_area: float = 0.0 # m² (coveredArea) cleaning_time: int = 0 # seconds # Map @@ -627,11 +627,13 @@ def is_returning(self) -> bool: def update_from_working_status(self, decoded: dict[str, Any]) -> None: """Update state from a decoded working_status message. - Confirmed via 35-min monitor capture (2026-02-27): - Field 3 = current session elapsed time (seconds) - (confirmed: 2136→2159 over 35-min clean) - Field 13 = cleaning area (cm²) — CONFIRMED (18000 = 1.8m²) - Field 15 = 600 during cleaning (purpose uncertain) + WorkingStatus proto fields (decompiled BuilderInfo): + Field 2 = coveredArea (float32, PbFieldType 0x100) — area cleaned this session, m² + Field 3 = timeConsuming (seconds) — session elapsed time + (confirmed: 2136→2159 over a 35-min clean) + + Field 13 is totalDryStationBagTime (cumulative station timer, 18000 = 5h), + not area — reading it as area is why the sensor was stuck at 1.8 m². """ self.raw_working_status = decoded if "3" in decoded: @@ -639,11 +641,10 @@ def update_from_working_status(self, decoded: dict[str, Any]) -> None: self.cleaning_time = int(decoded["3"]) except (ValueError, TypeError): pass - if "13" in decoded: - self.cleaning_area = int(decoded["13"]) - if "15" in decoded: - # Field 15 may be cumulative time; prefer field 3 for current session - pass + if "2" in decoded: + area = _to_float32(decoded["2"]) + if area is not None and area >= 0: + self.cleaning_area = area def update_from_base_status(self, decoded: dict[str, Any]) -> None: """Update state from a decoded robot_base_status message. diff --git a/tests/test_models.py b/tests/test_models.py index b707fdc..27fd479 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -29,9 +29,12 @@ def test_default_state(self) -> None: def test_update_from_working_status(self) -> None: """working_status topic sets cleaning metrics, not robot state.""" state = NarwalState() - state.update_from_working_status({"3": 120, "13": 18000, "15": 600}) + # Field 2 = coveredArea (float32, m²); field 13 = totalDryStationBagTime, ignored. + state.update_from_working_status( + {"2": _float_to_uint32(12.5), "3": 120, "13": 18000} + ) assert state.cleaning_time == 120 - assert state.cleaning_area == 18000 + assert state.cleaning_area == 12.5 # working_status is NOT set by this method (comes from base_status) assert state.working_status == WorkingStatus.UNKNOWN @@ -223,13 +226,13 @@ def test_incremental_updates(self) -> None: """State should accumulate across multiple topic updates.""" state = NarwalState() state.update_from_base_status({"3": {"1": 4}, "2": _float_to_uint32(95.0)}) - state.update_from_working_status({"3": 120, "13": 18000}) + state.update_from_working_status({"3": 120, "2": _float_to_uint32(12.5)}) state.update_from_upgrade_status({"7": "v01.02.19.02"}) assert state.battery_level == 95 assert state.is_cleaning assert state.cleaning_time == 120 - assert state.cleaning_area == 18000 + assert state.cleaning_area == 12.5 assert state.firmware_version == "v01.02.19.02" def test_raw_data_preserved(self) -> None: