roborock.devices.traits.v1.common
Module for Roborock V1 devices common trait commands.
This is an internal library and should not be used directly by consumers.
1"""Module for Roborock V1 devices common trait commands. 2 3This is an internal library and should not be used directly by consumers. 4""" 5 6import logging 7from abc import ABC, abstractmethod 8from dataclasses import fields 9from typing import ClassVar 10 11from roborock.data import RoborockBase 12from roborock.exceptions import RoborockParsingException 13from roborock.protocols.v1_protocol import V1RpcChannel 14from roborock.roborock_typing import RoborockCommand 15 16_LOGGER = logging.getLogger(__name__) 17 18 19V1ResponseData = dict | list | int | str 20 21 22class V1TraitDataConverter(ABC): 23 """Converts responses to RoborockBase objects. 24 25 This is an internal class and should not be used directly by consumers. 26 """ 27 28 @abstractmethod 29 def convert(self, response: V1ResponseData) -> RoborockBase: 30 """Convert the values to a dict that can be parsed as a RoborockBase.""" 31 32 def __repr__(self) -> str: 33 return self.__class__.__name__ 34 35 36class V1TraitMixin(ABC): 37 """Base model that supports v1 traits. 38 39 This class provides functioanlity for parsing responses from V1 devices 40 into dataclass instances. It also provides a reference to the V1RpcChannel 41 used to communicate with the device to execute commands. 42 43 Each trait subclass must define a class variable `command` that specifies 44 the RoborockCommand used to fetch the trait data from the device. The 45 `refresh()` method can be called to update the contents of the trait data 46 from the device. 47 48 A trait can also support additional commands for updating state associated 49 with the trait. It is expected that a trait will update its own internal 50 state either reflecting the change optimistically or by refreshing the 51 trait state from the device. In cases where one trait caches data that is 52 also represented in another trait, it is the responsibility of the caller 53 to ensure that both traits are refreshed as needed to keep them in sync. 54 55 The traits typically subclass RoborockBase to provide serialization 56 and deserialization functionality, but this is not strictly required. 57 """ 58 59 command: ClassVar[RoborockCommand] 60 """The RoborockCommand used to fetch the trait data from the device (internal only).""" 61 62 converter: V1TraitDataConverter 63 """The converter used to parse the response from the device (internal only).""" 64 65 def __init__(self) -> None: 66 """Initialize the V1TraitMixin.""" 67 self._rpc_channel: V1RpcChannel | None = None 68 69 @property 70 def rpc_channel(self) -> V1RpcChannel: 71 """Helper for executing commands, used internally by the trait""" 72 if not self._rpc_channel: 73 raise ValueError("Device trait in invalid state") 74 return self._rpc_channel 75 76 async def refresh(self) -> None: 77 """Refresh the contents of this trait.""" 78 response = await self.rpc_channel.send_command(self.command) 79 try: 80 new_data = self.converter.convert(response) 81 except (TypeError, ValueError) as err: 82 raise RoborockParsingException( 83 trait_name=type(self).__name__, 84 command=self.command, 85 payload=response, 86 inner_error=err, 87 ) from err 88 merge_trait_values(self, new_data) # type: ignore[arg-type] 89 90 91def merge_trait_values(target: RoborockBase, new_object: RoborockBase) -> bool: 92 """Update the target object with set fields in new_object.""" 93 updated = False 94 for field in fields(new_object): 95 old_value = getattr(target, field.name, None) 96 new_value = getattr(new_object, field.name, None) 97 if new_value != old_value: 98 setattr(target, field.name, new_value) 99 updated = True 100 return updated 101 102 103class DefaultConverter(V1TraitDataConverter): 104 """Converts responses to RoborockBase objects.""" 105 106 def __init__(self, dataclass_type: type[RoborockBase]) -> None: 107 """Initialize the converter.""" 108 self._dataclass_type = dataclass_type 109 110 def convert(self, response: V1ResponseData) -> RoborockBase: 111 """Convert the values to a dict that can be parsed as a RoborockBase. 112 113 Subclasses can override to implement custom parsing logic 114 """ 115 if isinstance(response, list): 116 response = response[0] 117 if not isinstance(response, dict): 118 raise ValueError(f"Unexpected {self._dataclass_type.__name__} response format: {response!r}") 119 return self._dataclass_type.from_dict(response) 120 121 122class SingleValueConverter(DefaultConverter): 123 """Base class for traits that represent a single value. 124 125 This class is intended to be subclassed by traits that represent a single 126 value, such as volume or brightness. The subclass should define a single 127 field with the metadata `roborock_value=True` to indicate which field 128 represents the main value of the trait. 129 """ 130 131 def __init__(self, dataclass_type: type[RoborockBase], value_field: str) -> None: 132 """Initialize the converter.""" 133 super().__init__(dataclass_type) 134 self._value_field = value_field 135 136 def convert(self, response: V1ResponseData) -> RoborockBase: 137 """Parse the response from the device into a RoborockValueBase.""" 138 if isinstance(response, list): 139 response = response[0] 140 if not isinstance(response, int): 141 raise ValueError(f"Unexpected response format: {response!r}") 142 return super().convert({self._value_field: response}) 143 144 145class RoborockSwitchBase(ABC): 146 """Base class for traits that represent a boolean switch.""" 147 148 @property 149 @abstractmethod 150 def is_on(self) -> bool: 151 """Return whether the switch is on.""" 152 153 @abstractmethod 154 async def enable(self) -> None: 155 """Enable the switch.""" 156 157 @abstractmethod 158 async def disable(self) -> None: 159 """Disable the switch.""" 160 161 162def mqtt_rpc_channel(cls): 163 """Decorator to mark a function as cloud only. 164 165 Normally a trait uses an adaptive rpc channel that can use either local 166 or cloud communication depending on what is available. This will force 167 the trait to always use the cloud rpc channel. 168 """ 169 170 def wrapper(*args, **kwargs): 171 return cls(*args, **kwargs) 172 173 cls.mqtt_rpc_channel = True # type: ignore[attr-defined] 174 return wrapper 175 176 177def map_rpc_channel(cls): 178 """Decorator to mark a function as cloud only using the map rpc format.""" 179 180 def wrapper(*args, **kwargs): 181 return cls(*args, **kwargs) 182 183 cls.map_rpc_channel = True # type: ignore[attr-defined] 184 return wrapper
23class V1TraitDataConverter(ABC): 24 """Converts responses to RoborockBase objects. 25 26 This is an internal class and should not be used directly by consumers. 27 """ 28 29 @abstractmethod 30 def convert(self, response: V1ResponseData) -> RoborockBase: 31 """Convert the values to a dict that can be parsed as a RoborockBase.""" 32 33 def __repr__(self) -> str: 34 return self.__class__.__name__
Converts responses to RoborockBase objects.
This is an internal class and should not be used directly by consumers.
29 @abstractmethod 30 def convert(self, response: V1ResponseData) -> RoborockBase: 31 """Convert the values to a dict that can be parsed as a RoborockBase."""
Convert the values to a dict that can be parsed as a RoborockBase.
37class V1TraitMixin(ABC): 38 """Base model that supports v1 traits. 39 40 This class provides functioanlity for parsing responses from V1 devices 41 into dataclass instances. It also provides a reference to the V1RpcChannel 42 used to communicate with the device to execute commands. 43 44 Each trait subclass must define a class variable `command` that specifies 45 the RoborockCommand used to fetch the trait data from the device. The 46 `refresh()` method can be called to update the contents of the trait data 47 from the device. 48 49 A trait can also support additional commands for updating state associated 50 with the trait. It is expected that a trait will update its own internal 51 state either reflecting the change optimistically or by refreshing the 52 trait state from the device. In cases where one trait caches data that is 53 also represented in another trait, it is the responsibility of the caller 54 to ensure that both traits are refreshed as needed to keep them in sync. 55 56 The traits typically subclass RoborockBase to provide serialization 57 and deserialization functionality, but this is not strictly required. 58 """ 59 60 command: ClassVar[RoborockCommand] 61 """The RoborockCommand used to fetch the trait data from the device (internal only).""" 62 63 converter: V1TraitDataConverter 64 """The converter used to parse the response from the device (internal only).""" 65 66 def __init__(self) -> None: 67 """Initialize the V1TraitMixin.""" 68 self._rpc_channel: V1RpcChannel | None = None 69 70 @property 71 def rpc_channel(self) -> V1RpcChannel: 72 """Helper for executing commands, used internally by the trait""" 73 if not self._rpc_channel: 74 raise ValueError("Device trait in invalid state") 75 return self._rpc_channel 76 77 async def refresh(self) -> None: 78 """Refresh the contents of this trait.""" 79 response = await self.rpc_channel.send_command(self.command) 80 try: 81 new_data = self.converter.convert(response) 82 except (TypeError, ValueError) as err: 83 raise RoborockParsingException( 84 trait_name=type(self).__name__, 85 command=self.command, 86 payload=response, 87 inner_error=err, 88 ) from err 89 merge_trait_values(self, new_data) # type: ignore[arg-type]
Base model that supports v1 traits.
This class provides functioanlity for parsing responses from V1 devices into dataclass instances. It also provides a reference to the V1RpcChannel used to communicate with the device to execute commands.
Each trait subclass must define a class variable command that specifies
the RoborockCommand used to fetch the trait data from the device. The
refresh() method can be called to update the contents of the trait data
from the device.
A trait can also support additional commands for updating state associated with the trait. It is expected that a trait will update its own internal state either reflecting the change optimistically or by refreshing the trait state from the device. In cases where one trait caches data that is also represented in another trait, it is the responsibility of the caller to ensure that both traits are refreshed as needed to keep them in sync.
The traits typically subclass RoborockBase to provide serialization and deserialization functionality, but this is not strictly required.
66 def __init__(self) -> None: 67 """Initialize the V1TraitMixin.""" 68 self._rpc_channel: V1RpcChannel | None = None
Initialize the V1TraitMixin.
The RoborockCommand used to fetch the trait data from the device (internal only).
The converter used to parse the response from the device (internal only).
70 @property 71 def rpc_channel(self) -> V1RpcChannel: 72 """Helper for executing commands, used internally by the trait""" 73 if not self._rpc_channel: 74 raise ValueError("Device trait in invalid state") 75 return self._rpc_channel
Helper for executing commands, used internally by the trait
77 async def refresh(self) -> None: 78 """Refresh the contents of this trait.""" 79 response = await self.rpc_channel.send_command(self.command) 80 try: 81 new_data = self.converter.convert(response) 82 except (TypeError, ValueError) as err: 83 raise RoborockParsingException( 84 trait_name=type(self).__name__, 85 command=self.command, 86 payload=response, 87 inner_error=err, 88 ) from err 89 merge_trait_values(self, new_data) # type: ignore[arg-type]
Refresh the contents of this trait.
92def merge_trait_values(target: RoborockBase, new_object: RoborockBase) -> bool: 93 """Update the target object with set fields in new_object.""" 94 updated = False 95 for field in fields(new_object): 96 old_value = getattr(target, field.name, None) 97 new_value = getattr(new_object, field.name, None) 98 if new_value != old_value: 99 setattr(target, field.name, new_value) 100 updated = True 101 return updated
Update the target object with set fields in new_object.
104class DefaultConverter(V1TraitDataConverter): 105 """Converts responses to RoborockBase objects.""" 106 107 def __init__(self, dataclass_type: type[RoborockBase]) -> None: 108 """Initialize the converter.""" 109 self._dataclass_type = dataclass_type 110 111 def convert(self, response: V1ResponseData) -> RoborockBase: 112 """Convert the values to a dict that can be parsed as a RoborockBase. 113 114 Subclasses can override to implement custom parsing logic 115 """ 116 if isinstance(response, list): 117 response = response[0] 118 if not isinstance(response, dict): 119 raise ValueError(f"Unexpected {self._dataclass_type.__name__} response format: {response!r}") 120 return self._dataclass_type.from_dict(response)
Converts responses to RoborockBase objects.
107 def __init__(self, dataclass_type: type[RoborockBase]) -> None: 108 """Initialize the converter.""" 109 self._dataclass_type = dataclass_type
Initialize the converter.
111 def convert(self, response: V1ResponseData) -> RoborockBase: 112 """Convert the values to a dict that can be parsed as a RoborockBase. 113 114 Subclasses can override to implement custom parsing logic 115 """ 116 if isinstance(response, list): 117 response = response[0] 118 if not isinstance(response, dict): 119 raise ValueError(f"Unexpected {self._dataclass_type.__name__} response format: {response!r}") 120 return self._dataclass_type.from_dict(response)
Convert the values to a dict that can be parsed as a RoborockBase.
Subclasses can override to implement custom parsing logic
123class SingleValueConverter(DefaultConverter): 124 """Base class for traits that represent a single value. 125 126 This class is intended to be subclassed by traits that represent a single 127 value, such as volume or brightness. The subclass should define a single 128 field with the metadata `roborock_value=True` to indicate which field 129 represents the main value of the trait. 130 """ 131 132 def __init__(self, dataclass_type: type[RoborockBase], value_field: str) -> None: 133 """Initialize the converter.""" 134 super().__init__(dataclass_type) 135 self._value_field = value_field 136 137 def convert(self, response: V1ResponseData) -> RoborockBase: 138 """Parse the response from the device into a RoborockValueBase.""" 139 if isinstance(response, list): 140 response = response[0] 141 if not isinstance(response, int): 142 raise ValueError(f"Unexpected response format: {response!r}") 143 return super().convert({self._value_field: response})
Base class for traits that represent a single value.
This class is intended to be subclassed by traits that represent a single
value, such as volume or brightness. The subclass should define a single
field with the metadata roborock_value=True to indicate which field
represents the main value of the trait.
132 def __init__(self, dataclass_type: type[RoborockBase], value_field: str) -> None: 133 """Initialize the converter.""" 134 super().__init__(dataclass_type) 135 self._value_field = value_field
Initialize the converter.
137 def convert(self, response: V1ResponseData) -> RoborockBase: 138 """Parse the response from the device into a RoborockValueBase.""" 139 if isinstance(response, list): 140 response = response[0] 141 if not isinstance(response, int): 142 raise ValueError(f"Unexpected response format: {response!r}") 143 return super().convert({self._value_field: response})
Parse the response from the device into a RoborockValueBase.
146class RoborockSwitchBase(ABC): 147 """Base class for traits that represent a boolean switch.""" 148 149 @property 150 @abstractmethod 151 def is_on(self) -> bool: 152 """Return whether the switch is on.""" 153 154 @abstractmethod 155 async def enable(self) -> None: 156 """Enable the switch.""" 157 158 @abstractmethod 159 async def disable(self) -> None: 160 """Disable the switch."""
Base class for traits that represent a boolean switch.
149 @property 150 @abstractmethod 151 def is_on(self) -> bool: 152 """Return whether the switch is on."""
Return whether the switch is on.
163def mqtt_rpc_channel(cls): 164 """Decorator to mark a function as cloud only. 165 166 Normally a trait uses an adaptive rpc channel that can use either local 167 or cloud communication depending on what is available. This will force 168 the trait to always use the cloud rpc channel. 169 """ 170 171 def wrapper(*args, **kwargs): 172 return cls(*args, **kwargs) 173 174 cls.mqtt_rpc_channel = True # type: ignore[attr-defined] 175 return wrapper
Decorator to mark a function as cloud only.
Normally a trait uses an adaptive rpc channel that can use either local or cloud communication depending on what is available. This will force the trait to always use the cloud rpc channel.
178def map_rpc_channel(cls): 179 """Decorator to mark a function as cloud only using the map rpc format.""" 180 181 def wrapper(*args, **kwargs): 182 return cls(*args, **kwargs) 183 184 cls.map_rpc_channel = True # type: ignore[attr-defined] 185 return wrapper
Decorator to mark a function as cloud only using the map rpc format.