1717>>> config_dict = device.config.to_dict()
1818>>> # DeviceConfig.to_dict() can be used to store for later
1919>>> print(config_dict)
20- {'host': '127.0.0.3', 'timeout': 5, 'credentials': Credentials(), 'connection_type'\
21- : {'device_family': 'SMART.TAPOBULB', 'encryption_type': 'KLAP', 'https': False, \
22- 'login_version': 2}, 'uses_http': True}
20+ {'host': '127.0.0.3', 'timeout': 5, 'credentials': {'username': 'user@example.com', \
21+ 'password': 'great_password'}, 'connection_type'\
22+ : {'device_family': 'SMART.TAPOBULB', 'encryption_type': 'KLAP', 'login_version': 2, \
23+ 'https': False}, 'uses_http': True}
2324
2425>>> later_device = await Device.connect(config=Device.Config.from_dict(config_dict))
2526>>> print(later_device.alias) # Alias is available as connect() calls update()
2627Living Room Bulb
2728
2829"""
2930
30- # Module cannot use from __future__ import annotations until migrated to mashumaru
31- # as dataclass.fields() will not resolve the type.
31+ from __future__ import annotations
32+
3233import logging
33- from dataclasses import asdict , dataclass , field , fields , is_dataclass
34+ from dataclasses import dataclass , field , replace
3435from enum import Enum
35- from typing import TYPE_CHECKING , Any , Optional , TypedDict
36+ from typing import TYPE_CHECKING , Any , Self , TypedDict
37+
38+ from aiohttp import ClientSession
39+ from mashumaro import field_options
40+ from mashumaro .config import BaseConfig
41+ from mashumaro .types import SerializationStrategy
3642
3743from .credentials import Credentials
3844from .exceptions import KasaException
45+ from .json import DataClassJSONMixin
3946
4047if TYPE_CHECKING :
4148 from aiohttp import ClientSession
@@ -73,45 +80,17 @@ class DeviceFamily(Enum):
7380 SmartIpCamera = "SMART.IPCAMERA"
7481
7582
76- def _dataclass_from_dict (klass : Any , in_val : dict ) -> Any :
77- if is_dataclass (klass ):
78- fieldtypes = {f .name : f .type for f in fields (klass )}
79- val = {}
80- for dict_key in in_val :
81- if dict_key in fieldtypes :
82- if hasattr (fieldtypes [dict_key ], "from_dict" ):
83- val [dict_key ] = fieldtypes [dict_key ].from_dict (in_val [dict_key ]) # type: ignore[union-attr]
84- else :
85- val [dict_key ] = _dataclass_from_dict (
86- fieldtypes [dict_key ], in_val [dict_key ]
87- )
88- else :
89- raise KasaException (
90- f"Cannot create dataclass from dict, unknown key: { dict_key } "
91- )
92- return klass (** val ) # type: ignore[operator]
93- else :
94- return in_val
95-
96-
97- def _dataclass_to_dict (in_val : Any ) -> dict :
98- fieldtypes = {f .name : f .type for f in fields (in_val ) if f .compare }
99- out_val = {}
100- for field_name in fieldtypes :
101- val = getattr (in_val , field_name )
102- if val is None :
103- continue
104- elif hasattr (val , "to_dict" ):
105- out_val [field_name ] = val .to_dict ()
106- elif is_dataclass (fieldtypes [field_name ]):
107- out_val [field_name ] = asdict (val )
108- else :
109- out_val [field_name ] = val
110- return out_val
83+ class _DeviceConfigBaseMixin (DataClassJSONMixin ):
84+ """Base class for serialization mixin."""
85+
86+ class Config (BaseConfig ):
87+ """Serialization config."""
88+
89+ omit_none = True
11190
11291
11392@dataclass
114- class DeviceConnectionParameters :
93+ class DeviceConnectionParameters ( _DeviceConfigBaseMixin ) :
11594 """Class to hold the the parameters determining connection type."""
11695
11796 device_family : DeviceFamily
@@ -125,7 +104,7 @@ def from_values(
125104 encryption_type : str ,
126105 login_version : int | None = None ,
127106 https : bool | None = None ,
128- ) -> " DeviceConnectionParameters" :
107+ ) -> DeviceConnectionParameters :
129108 """Return connection parameters from string values."""
130109 try :
131110 if https is None :
@@ -142,39 +121,17 @@ def from_values(
142121 + f"{ encryption_type } .{ login_version } "
143122 ) from ex
144123
145- @staticmethod
146- def from_dict (connection_type_dict : dict [str , Any ]) -> "DeviceConnectionParameters" :
147- """Return connection parameters from dict."""
148- if (
149- isinstance (connection_type_dict , dict )
150- and (device_family := connection_type_dict .get ("device_family" ))
151- and (encryption_type := connection_type_dict .get ("encryption_type" ))
152- ):
153- if login_version := connection_type_dict .get ("login_version" ):
154- login_version = int (login_version ) # type: ignore[assignment]
155- return DeviceConnectionParameters .from_values (
156- device_family ,
157- encryption_type ,
158- login_version , # type: ignore[arg-type]
159- connection_type_dict .get ("https" , False ),
160- )
161124
162- raise KasaException (f"Invalid connection type data for { connection_type_dict } " )
125+ class _DoNotSerialize (SerializationStrategy ):
126+ def serialize (self , value : Any ) -> None :
127+ return None # pragma: no cover
163128
164- def to_dict (self ) -> dict [str , str | int | bool ]:
165- """Convert connection params to dict."""
166- result : dict [str , str | int ] = {
167- "device_family" : self .device_family .value ,
168- "encryption_type" : self .encryption_type .value ,
169- "https" : self .https ,
170- }
171- if self .login_version :
172- result ["login_version" ] = self .login_version
173- return result
129+ def deserialize (self , value : Any ) -> None :
130+ return None # pragma: no cover
174131
175132
176133@dataclass
177- class DeviceConfig :
134+ class DeviceConfig ( _DeviceConfigBaseMixin ) :
178135 """Class to represent paramaters that determine how to connect to devices."""
179136
180137 DEFAULT_TIMEOUT = 5
@@ -202,9 +159,12 @@ class DeviceConfig:
202159 #: in order to determine whether they should pass a custom http client if desired.
203160 uses_http : bool = False
204161
205- # compare=False will be excluded from the serialization and object comparison.
206162 #: Set a custom http_client for the device to use.
207- http_client : Optional ["ClientSession" ] = field (default = None , compare = False )
163+ http_client : ClientSession | None = field (
164+ default = None ,
165+ compare = False ,
166+ metadata = field_options (serialization_strategy = _DoNotSerialize ()),
167+ )
208168
209169 aes_keys : KeyPairDict | None = None
210170
@@ -214,22 +174,30 @@ def __post_init__(self) -> None:
214174 DeviceFamily .IotSmartPlugSwitch , DeviceEncryptionType .Xor
215175 )
216176
217- def to_dict (
177+ def __pre_serialize__ (self ) -> Self :
178+ return replace (self , http_client = None )
179+
180+ def to_dict_control_credentials (
218181 self ,
219182 * ,
220183 credentials_hash : str | None = None ,
221184 exclude_credentials : bool = False ,
222185 ) -> dict [str , dict [str , str ]]:
223- """Convert device config to dict."""
224- if credentials_hash is not None or exclude_credentials :
225- self .credentials = None
226- if credentials_hash :
227- self .credentials_hash = credentials_hash
228- return _dataclass_to_dict (self )
186+ """Convert deviceconfig to dict controlling how to serialize credentials.
187+
188+ If credentials_hash is provided credentials will be None.
189+ If credentials_hash is '' credentials_hash and credentials will be None.
190+ exclude credentials controls whether to include credentials.
191+ The defaults are the same as calling to_dict().
192+ """
193+ if credentials_hash is None :
194+ if not exclude_credentials :
195+ return self .to_dict ()
196+ else :
197+ return replace (self , credentials = None ).to_dict ()
229198
230- @staticmethod
231- def from_dict (config_dict : dict [str , dict [str , str ]]) -> "DeviceConfig" :
232- """Return device config from dict."""
233- if isinstance (config_dict , dict ):
234- return _dataclass_from_dict (DeviceConfig , config_dict )
235- raise KasaException (f"Invalid device config data: { config_dict } " )
199+ return replace (
200+ self ,
201+ credentials_hash = credentials_hash if credentials_hash else None ,
202+ credentials = None ,
203+ ).to_dict ()
0 commit comments