55
66import logging
77from abc import ABC , abstractmethod
8- from dataclasses import dataclass , fields
9- from typing import ClassVar , Self
8+ from dataclasses import fields
9+ from typing import ClassVar
1010
1111from roborock .data import RoborockBase
1212from roborock .protocols .v1_protocol import V1RpcChannel
1313from roborock .roborock_typing import RoborockCommand
1414
1515_LOGGER = logging .getLogger (__name__ )
1616
17+
1718V1ResponseData = dict | list | int | str
1819
1920
20- @dataclass
21+ class V1TraitDataConverter (ABC ):
22+ """Converts responses to RoborockBase objects.
23+
24+ This is an internal class and should not be used directly by consumers.
25+ """
26+
27+ @abstractmethod
28+ def convert (self , response : V1ResponseData ) -> RoborockBase :
29+ """Convert the values to a dict that can be parsed as a RoborockBase."""
30+
31+ def __repr__ (self ) -> str :
32+ return self .__class__ .__name__
33+
34+
2135class V1TraitMixin (ABC ):
2236 """Base model that supports v1 traits.
2337
@@ -42,37 +56,13 @@ class V1TraitMixin(ABC):
4256 """
4357
4458 command : ClassVar [RoborockCommand ]
59+ """The RoborockCommand used to fetch the trait data from the device (internal only)."""
4560
46- @classmethod
47- def _parse_type_response (cls , response : V1ResponseData ) -> RoborockBase :
48- """Parse the response from the device into a a RoborockBase.
49-
50- Subclasses should override this method to implement custom parsing
51- logic as needed.
52- """
53- if not issubclass (cls , RoborockBase ):
54- raise NotImplementedError (f"Trait { cls } does not implement RoborockBase" )
55- # Subclasses can override to implement custom parsing logic
56- if isinstance (response , list ):
57- response = response [0 ]
58- if not isinstance (response , dict ):
59- raise ValueError (f"Unexpected { cls } response format: { response !r} " )
60- return cls .from_dict (response )
61-
62- def _parse_response (self , response : V1ResponseData ) -> RoborockBase :
63- """Parse the response from the device into a a RoborockBase.
64-
65- This is used by subclasses that want to override the class
66- behavior with instance-specific data.
67- """
68- return self ._parse_type_response (response )
69-
70- def __post_init__ (self ) -> None :
71- """Post-initialization to set up the RPC channel.
61+ converter : V1TraitDataConverter
62+ """The converter used to parse the response from the device (internal only)."""
7263
73- This is called automatically after the dataclass is initialized by the
74- device setup code.
75- """
64+ def __init__ (self ) -> None :
65+ """Initialize the V1TraitMixin."""
7666 self ._rpc_channel = None
7767
7868 @property
@@ -85,32 +75,42 @@ def rpc_channel(self) -> V1RpcChannel:
8575 async def refresh (self ) -> None :
8676 """Refresh the contents of this trait."""
8777 response = await self .rpc_channel .send_command (self .command )
88- new_data = self ._parse_response (response )
89- if not isinstance (new_data , RoborockBase ):
90- raise ValueError (f"Internal error, unexpected response type: { new_data !r} " )
91- _LOGGER .debug ("Refreshed %s: %s" , self .__class__ .__name__ , new_data )
92- self ._update_trait_values (new_data )
93-
94- def _update_trait_values (self , new_data : RoborockBase ) -> None :
95- """Update the values of this trait from another instance."""
96- for field in fields (new_data ):
97- new_value = getattr (new_data , field .name , None )
98- setattr (self , field .name , new_value )
99-
100-
101- def _get_value_field (clazz : type [V1TraitMixin ]) -> str :
102- """Get the name of the field marked as the main value of the RoborockValueBase."""
103- value_fields = [field .name for field in fields (clazz ) if field .metadata .get ("roborock_value" , False )]
104- if len (value_fields ) != 1 :
105- raise ValueError (
106- f"RoborockValueBase subclass { clazz } must have exactly one field marked as roborock_value, "
107- f" but found: { value_fields } "
108- )
109- return value_fields [0 ]
110-
111-
112- @dataclass (init = False , kw_only = True )
113- class RoborockValueBase (V1TraitMixin , RoborockBase ):
78+ new_data = self .converter .convert (response )
79+ merge_trait_values (self , new_data ) # type: ignore[arg-type]
80+
81+
82+ def merge_trait_values (target : RoborockBase , new_object : RoborockBase ) -> bool :
83+ """Update the target object with set fields in new_object."""
84+ updated = False
85+ for field in fields (new_object ):
86+ old_value = getattr (target , field .name , None )
87+ new_value = getattr (new_object , field .name , None )
88+ if new_value != old_value :
89+ setattr (target , field .name , new_value )
90+ updated = True
91+ return updated
92+
93+
94+ class DefaultConverter (V1TraitDataConverter ):
95+ """Converts responses to RoborockBase objects."""
96+
97+ def __init__ (self , dataclass_type : type [RoborockBase ]) -> None :
98+ """Initialize the converter."""
99+ self ._dataclass_type = dataclass_type
100+
101+ def convert (self , response : V1ResponseData ) -> RoborockBase :
102+ """Convert the values to a dict that can be parsed as a RoborockBase.
103+
104+ Subclasses can override to implement custom parsing logic
105+ """
106+ if isinstance (response , list ):
107+ response = response [0 ]
108+ if not isinstance (response , dict ):
109+ raise ValueError (f"Unexpected { self ._dataclass_type .__name__ } response format: { response !r} " )
110+ return self ._dataclass_type .from_dict (response )
111+
112+
113+ class SingleValueConverter (DefaultConverter ):
114114 """Base class for traits that represent a single value.
115115
116116 This class is intended to be subclassed by traits that represent a single
@@ -119,15 +119,18 @@ class RoborockValueBase(V1TraitMixin, RoborockBase):
119119 represents the main value of the trait.
120120 """
121121
122- @classmethod
123- def _parse_response (cls , response : V1ResponseData ) -> Self :
122+ def __init__ (self , dataclass_type : type [RoborockBase ], value_field : str ) -> None :
123+ """Initialize the converter."""
124+ super ().__init__ (dataclass_type )
125+ self ._value_field = value_field
126+
127+ def convert (self , response : V1ResponseData ) -> RoborockBase :
124128 """Parse the response from the device into a RoborockValueBase."""
125129 if isinstance (response , list ):
126130 response = response [0 ]
127131 if not isinstance (response , int ):
128132 raise ValueError (f"Unexpected response format: { response !r} " )
129- value_field = _get_value_field (cls )
130- return cls (** {value_field : response })
133+ return super ().convert ({self ._value_field : response })
131134
132135
133136class RoborockSwitchBase (ABC ):
0 commit comments