Skip to content

_init_data() clears _capability and _aes_iv, permanently breaking map decoding after an empty map #1707

Description

@Mizukamakiri

Version: v2.0.0b25 Device: Dreame X60 Ultra (Dreamehome cloud account, dreame_cloud protocol) HA: HAOS

Summary
On a freshly-added device that has no saved map yet, the first map frame is an empty (2×2) map. Handling that frame calls _init_data(), which nulls _capability and _aes_iv. Nothing ever restores them, so every subsequent map decode raises AttributeError and fails the whole coordinator update. The device never recovers until the integration is reloaded.

Traceback

WARNING (Thread-4208) [custom_components.dreame_vacuum.dreame.map] Map update Failed: Traceback (most recent call last):
  File ".../dreame/map.py", line 1512, in update
  File ".../dreame/map.py", line 452, in _request_current_map
  File ".../dreame/map.py", line 350, in _request_i_map
  File ".../dreame/map.py", line 681, in _add_map_data_file
  File ".../dreame/map.py", line 684, in _add_raw_map_data
  File ".../dreame/map.py", line 896, in _add_map_data
  File ".../dreame/map.py", line 6658, in set_floor_material
  File ".../dreame/map.py", line 6610, in set_segment_floor_material
ERROR (MainThread) [custom_components.dreame_vacuum] Error requesting dreame_vacuum data:
    'NoneType' object has no attribute 'floor_direction_cleaning'

Root cause
_init_data() is the map-state reset, but it also clears three device-level fields:

map.py:174

def _init_data(self) -> None:
    ...
    self._aes_iv: str = None      # 201
    self._aes_key: str = None     # 202
    self._capability: DreameVacuumDeviceCapability = None   # 203

_capability and _aes_iv are assigned only in set_capability():

map.py:1530

def set_capability(self, capability) -> None:
    self._capability = capability
    self._aes_iv = capability.key

…which is called only from device.py:498 (construction) and device.py:641 (guarded by if not self.capability.loaded:, so effectively once). Neither runs again during the device's lifetime.

_add_map_data calls _init_data() when it sees the first empty map:

map.py:814

if map_data.empty_map:
    if self._map_data is None or not self._map_data.empty_map:
        self._init_data()          # <-- wipes _capability and _aes_iv

On a new device with no saved map, map_data.empty_map is True (2×2, per map.py:4464) and self._map_data is None on the first frame after startup, so this always fires. Once the robot builds a real map, the next non-empty I-frame reaches:

map.py:896

DreameVacuumMapDecoder.set_floor_material(map_data, self._capability, saved_map_data)   # None

and set_segment_floor_material dereferences it unguarded:

map.py:6610

if capability.floor_direction_cleaning and material == 1:

This is the only unguarded capability access in that function — line 6604 uses capability is not None and ... and line 6639 uses elif capability and capability.floor_material:.

Impact
The AttributeError propagates to update(), is caught at map.py:1520, sets _available = False and fires _error_callback(DeviceUpdateFailedException(ex)). That fails the entire coordinator refresh, so all entities for the device go unavailable — not just map entities — even while the robot is actively cleaning. The camera serves the "no map" placeholder, calibration_points stay as all-zero stubs, and no rooms attribute is exposed, so xiaomi-vacuum-map-card room selection cannot work.

Because nothing re-calls set_capability(), this state is permanent until reload.

Note _aes_iv is also lost, which affects map decryption (used at lines 614, 1312, 1363, 1668, 2713). _aes_key is fine to reset — get_aes_key() re-derives it lazily via the if self._aes_key is None check at line 1179.

Suggested fix
Move the two device-level fields out of the map-state reset:

     def __init__(self, _protocol: DreameVacuumProtocol) -> None:
         ...
         self._connected: bool = True
+
+        self._aes_iv: str = None
+        self._capability: DreameVacuumDeviceCapability = None
 
         self._init_data()
 
     def _init_data(self) -> None:
         ...
-        self._aes_iv: str = None
         self._aes_key: str = None
-        self._capability: DreameVacuumDeviceCapability = None

Guarding line 6610 (if capability is not None and capability.floor_direction_cleaning ...) would additionally match the defensive style used at 6604/6639, but it treats the symptom — _aes_iv would still be silently lost.

Reproduce
Add a device that has no saved map (factory reset / first setup).
Let it send an empty map, then start a mapping run.
Every map update fails from the first non-empty I-frame onward; all entities go unavailable.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingquestionFurther information is requested

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions