17
17
>>> config_dict = device.config.to_dict()
18
18
>>> # DeviceConfig.to_dict() can be used to store for later
19
19
>>> 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}
23
24
24
25
>>> later_device = await Device.connect(config=Device.Config.from_dict(config_dict))
25
26
>>> print(later_device.alias) # Alias is available as connect() calls update()
26
27
Living Room Bulb
27
28
28
29
"""
29
30
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
+
32
33
import logging
33
- from dataclasses import asdict , dataclass , field , fields , is_dataclass
34
+ from dataclasses import dataclass , field , replace
34
35
from 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
36
42
37
43
from .credentials import Credentials
38
44
from .exceptions import KasaException
45
+ from .json import DataClassJSONMixin
39
46
40
47
if TYPE_CHECKING :
41
48
from aiohttp import ClientSession
@@ -73,45 +80,17 @@ class DeviceFamily(Enum):
73
80
SmartIpCamera = "SMART.IPCAMERA"
74
81
75
82
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
111
90
112
91
113
92
@dataclass
114
- class DeviceConnectionParameters :
93
+ class DeviceConnectionParameters ( _DeviceConfigBaseMixin ) :
115
94
"""Class to hold the the parameters determining connection type."""
116
95
117
96
device_family : DeviceFamily
@@ -125,7 +104,7 @@ def from_values(
125
104
encryption_type : str ,
126
105
login_version : int | None = None ,
127
106
https : bool | None = None ,
128
- ) -> " DeviceConnectionParameters" :
107
+ ) -> DeviceConnectionParameters :
129
108
"""Return connection parameters from string values."""
130
109
try :
131
110
if https is None :
@@ -142,39 +121,17 @@ def from_values(
142
121
+ f"{ encryption_type } .{ login_version } "
143
122
) from ex
144
123
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
- )
161
124
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
163
128
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
174
131
175
132
176
133
@dataclass
177
- class DeviceConfig :
134
+ class DeviceConfig ( _DeviceConfigBaseMixin ) :
178
135
"""Class to represent paramaters that determine how to connect to devices."""
179
136
180
137
DEFAULT_TIMEOUT = 5
@@ -202,9 +159,12 @@ class DeviceConfig:
202
159
#: in order to determine whether they should pass a custom http client if desired.
203
160
uses_http : bool = False
204
161
205
- # compare=False will be excluded from the serialization and object comparison.
206
162
#: 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
+ )
208
168
209
169
aes_keys : KeyPairDict | None = None
210
170
@@ -214,22 +174,30 @@ def __post_init__(self) -> None:
214
174
DeviceFamily .IotSmartPlugSwitch , DeviceEncryptionType .Xor
215
175
)
216
176
217
- def to_dict (
177
+ def __pre_serialize__ (self ) -> Self :
178
+ return replace (self , http_client = None )
179
+
180
+ def to_dict_control_credentials (
218
181
self ,
219
182
* ,
220
183
credentials_hash : str | None = None ,
221
184
exclude_credentials : bool = False ,
222
185
) -> 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 ()
229
198
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