55
66import logging
77from abc import ABC , abstractmethod
8- from dataclasses import dataclass , fields
9- from typing import Any , ClassVar , Self
8+ from dataclasses import fields
9+ from typing import ClassVar
1010
11- from roborock .callbacks import CallbackList
1211from roborock .data import RoborockBase
1312from roborock .protocols .v1_protocol import V1RpcChannel
1413from roborock .roborock_typing import RoborockCommand
15- from roborock .devices .traits .common import TraitDataConverter , TraitUpdateListener
1614
1715_LOGGER = logging .getLogger (__name__ )
1816
1917V1ResponseData = dict | list | int | str
2018
19+
2120class V1TraitDataConverter :
22- """Utility to handle the transformation and merging of data into models.
21+ """Converts responses to RoborockBase objects."""
2322
24- This has some special handling for v1 responses, then uses the common data
25- converters for updating the target objects.
26- """
27-
28- def __init__ (self , dataclass_type : type [RoborockBase ]):
29- """Initialize the converter for a specific RoborockBase-derived class."""
30- self ._dataclass_type = dataclass_type
31- self ._converter = TraitDataConverter (dataclass_type )
23+ @abstractmethod
24+ def convert (self , response : V1ResponseData ) -> RoborockBase :
25+ """Convert the values to a dict that can be parsed as a RoborockBase."""
3226
33- def update_from_data (self , target : RoborockBase , data : V1ResponseData ) -> None :
34- """Update the target object from a dictionary of raw values.
35-
36- Returns True if any values were updated.
37- """
38- response = self ._convert_to_dict (data )
39- return self ._converter .update_from_dict (target , response )
40-
41- def _convert_to_dict (self , response : V1ResponseData ) -> None :
42- """Convert the values to a dict that can be parsed as a RoborockBase.
43-
44- Subclasses can override to implement custom parsing logic
45- """
46- if isinstance (response , list ):
47- response = response [0 ]
48- if not isinstance (response , dict ):
49- raise ValueError (f"Unexpected { self ._dataclass_type .__name__ } response format: { response !r} " )
50- return response
27+ def __repr__ (self ) -> str :
28+ return self .__class__ .__name__
5129
5230
5331class V1TraitMixin (ABC ):
@@ -74,13 +52,11 @@ class V1TraitMixin(ABC):
7452 """
7553
7654 command : ClassVar [RoborockCommand ]
77- converter : ClassVar [ V1TraitDataConverter ]
55+ converter : V1TraitDataConverter
7856
7957 def __init__ (self ) -> None :
8058 """Initialize the V1TraitMixin."""
8159 self ._rpc_channel = None
82- self ._update_listener = TraitUpdateListener (_LOGGER )
83-
8460
8561 @property
8662 def rpc_channel (self ) -> V1RpcChannel :
@@ -92,36 +68,43 @@ def rpc_channel(self) -> V1RpcChannel:
9268 async def refresh (self ) -> None :
9369 """Refresh the contents of this trait."""
9470 response = await self .rpc_channel .send_command (self .command )
95- self ._update_data (response )
71+ new_data = self .converter .convert (response )
72+ # Mixin doesn't know its type
73+ merge_object_values (self , new_data ) # type: ignore[arg-type]
9674
97- def _update_data (self , response : Any ) -> None :
98- """Applies the new data to the object and notifies listeners.
99-
100- This can be overridden by subclasses to change the behavior, but it is
101- preferred to just use the `converter`.
102- """
103- if self .converter .update_from_data (self , response ):
104- self ._update_listener ._notify_update ()
10575
106- def add_update_listener (self , callback : Callable [[], None ]) -> Callable [[], None ]:
107- """Register a callback when the trait has been updated.
76+ def merge_object_values (target : RoborockBase , new_object : RoborockBase ) -> bool :
77+ """Update the target object with set fields in new_object."""
78+ updated = False
79+ for field in fields (new_object ):
80+ old_value = getattr (target , field .name , None )
81+ new_value = getattr (new_object , field .name , None )
82+ if new_value != old_value :
83+ setattr (target , field .name , new_value )
84+ updated = True
85+ return updated
10886
109- Returns a callable to remove the listener.
110- """
111- self ._update_listener .add_update_listener (callback )
11287
88+ class DefaultConverter (V1TraitDataConverter ):
89+ """Converts responses to RoborockBase objects."""
90+
91+ def __init__ (self , dataclass_type : type [RoborockBase ]) -> None :
92+ """Initialize the converter."""
93+ self ._dataclass_type = dataclass_type
11394
114- def _get_value_field (clazz : type [V1TraitMixin ]) -> str :
115- """Get the name of the field marked as the main value of the RoborockValueBase."""
116- value_fields = [field .name for field in fields (clazz ) if field .metadata .get ("roborock_value" , False )]
117- if len (value_fields ) != 1 :
118- raise ValueError (
119- f"RoborockValueBase subclass { clazz } must have exactly one field marked as roborock_value, "
120- f" but found: { value_fields } "
121- )
122- return value_fields [0 ]
95+ def convert (self , response : V1ResponseData ) -> RoborockBase :
96+ """Convert the values to a dict that can be parsed as a RoborockBase.
12397
124- class SingleValueConverter (V1TraitDataConverter ):
98+ Subclasses can override to implement custom parsing logic
99+ """
100+ if isinstance (response , list ):
101+ response = response [0 ]
102+ if not isinstance (response , dict ):
103+ raise ValueError (f"Unexpected { self ._dataclass_type .__name__ } response format: { response !r} " )
104+ return self ._dataclass_type .from_dict (response )
105+
106+
107+ class SingleValueConverter (DefaultConverter ):
125108 """Base class for traits that represent a single value.
126109
127110 This class is intended to be subclassed by traits that represent a single
@@ -135,15 +118,14 @@ def __init__(self, dataclass_type: type[RoborockBase], value_field: str) -> None
135118 super ().__init__ (dataclass_type )
136119 self ._value_field = value_field
137120
138- @classmethod
139- def _parse_response (cls , response : V1ResponseData ) -> Self :
121+ def convert (self , response : V1ResponseData ) -> RoborockBase :
140122 """Parse the response from the device into a RoborockValueBase."""
141123 if isinstance (response , list ):
142124 response = response [0 ]
143125 if not isinstance (response , int ):
144126 raise ValueError (f"Unexpected response format: { response !r} " )
145- value_field = _get_value_field ( cls )
146- return cls ( ** { self . __value_field : response })
127+ return super (). convert ({ self . _value_field : response } )
128+
147129
148130class RoborockSwitchBase (ABC ):
149131 """Base class for traits that represent a boolean switch."""
0 commit comments