daybetter-services-python 1.0.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- daybetter_python/__init__.py +7 -0
- daybetter_python/client.py +466 -0
- daybetter_python/exceptions.py +38 -0
- daybetter_python/py.typed +0 -0
- daybetter_services_python-1.0.7.dist-info/METADATA +99 -0
- daybetter_services_python-1.0.7.dist-info/RECORD +9 -0
- daybetter_services_python-1.0.7.dist-info/WHEEL +5 -0
- daybetter_services_python-1.0.7.dist-info/licenses/LICENSE +21 -0
- daybetter_services_python-1.0.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
"""DayBetter API client."""
|
|
2
|
+
|
|
3
|
+
import aiohttp
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple, Type
|
|
6
|
+
|
|
7
|
+
from .exceptions import DayBetterError, AuthenticationError, APIError
|
|
8
|
+
|
|
9
|
+
_LOGGER = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DayBetterClient:
|
|
13
|
+
"""DayBetter API client."""
|
|
14
|
+
|
|
15
|
+
# 测试环境URL
|
|
16
|
+
TEST_BASE_URL = "https://cloud.v2.dbiot.link/daybetter/hass/api/v1.0/"
|
|
17
|
+
# 正式环境URL
|
|
18
|
+
PROD_BASE_URL = "https://a.dbiot.org/daybetter/hass/api/v1.0/"
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
token: str,
|
|
23
|
+
base_url: Optional[str] = None,
|
|
24
|
+
hass_code: Optional[str] = None
|
|
25
|
+
):
|
|
26
|
+
"""Initialize the client.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
token: Authentication token
|
|
30
|
+
base_url: Base URL for the API (optional, will be determined by hass_code if not provided)
|
|
31
|
+
hass_code: Home Assistant integration code (optional, if provided and starts with "db-",
|
|
32
|
+
will use production environment)
|
|
33
|
+
"""
|
|
34
|
+
self.token = token
|
|
35
|
+
|
|
36
|
+
# 根据 hass_code 或 base_url 确定使用的环境
|
|
37
|
+
if base_url is not None:
|
|
38
|
+
# 如果明确指定了 base_url,使用指定的 URL
|
|
39
|
+
self.base_url = base_url
|
|
40
|
+
elif hass_code is not None and hass_code.startswith("db-"):
|
|
41
|
+
# 如果 hass_code 以 "db-" 开头,使用正式环境
|
|
42
|
+
self.base_url = self.PROD_BASE_URL
|
|
43
|
+
_LOGGER.debug("Using production environment based on hass_code")
|
|
44
|
+
else:
|
|
45
|
+
# 默认使用测试环境
|
|
46
|
+
self.base_url = self.TEST_BASE_URL
|
|
47
|
+
_LOGGER.debug("Using test environment")
|
|
48
|
+
|
|
49
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
50
|
+
self._auth_valid = True
|
|
51
|
+
self._devices: List[Dict[str, Any]] = []
|
|
52
|
+
self._pids: Dict[str, Any] = {}
|
|
53
|
+
|
|
54
|
+
async def __aenter__(self):
|
|
55
|
+
"""Async context manager entry."""
|
|
56
|
+
self._session = aiohttp.ClientSession()
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
async def __aexit__(
|
|
60
|
+
self,
|
|
61
|
+
exc_type: Optional[Type[BaseException]],
|
|
62
|
+
exc_val: Optional[BaseException],
|
|
63
|
+
exc_tb: Optional[Any],
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Async context manager exit."""
|
|
66
|
+
if self._session:
|
|
67
|
+
await self._session.close()
|
|
68
|
+
self._session = None
|
|
69
|
+
|
|
70
|
+
def _get_session(self) -> aiohttp.ClientSession:
|
|
71
|
+
"""Get or create aiohttp session."""
|
|
72
|
+
if not self._session:
|
|
73
|
+
self._session = aiohttp.ClientSession()
|
|
74
|
+
return self._session
|
|
75
|
+
|
|
76
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
77
|
+
"""Get request headers."""
|
|
78
|
+
return {"Authorization": f"Bearer {self.token}"}
|
|
79
|
+
|
|
80
|
+
async def fetch_devices(self) -> List[Dict[str, Any]]:
|
|
81
|
+
"""Fetch devices from API.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
List of device dictionaries
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
AuthenticationError: If authentication fails
|
|
88
|
+
APIError: If API request fails
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
session = self._get_session()
|
|
92
|
+
url = f"{self.base_url}hass/devices"
|
|
93
|
+
headers = self._get_headers()
|
|
94
|
+
|
|
95
|
+
async with session.post(url, headers=headers) as resp:
|
|
96
|
+
if resp.status == 200:
|
|
97
|
+
data = await resp.json()
|
|
98
|
+
devices = data.get("data", [])
|
|
99
|
+
_LOGGER.debug("Fetched %d devices", len(devices))
|
|
100
|
+
self._auth_valid = True
|
|
101
|
+
return devices
|
|
102
|
+
elif resp.status == 401:
|
|
103
|
+
_LOGGER.error("Authentication failed - token may be expired")
|
|
104
|
+
self._auth_valid = False
|
|
105
|
+
raise AuthenticationError("Authentication failed - token may be expired")
|
|
106
|
+
else:
|
|
107
|
+
error_text = await resp.text()
|
|
108
|
+
_LOGGER.error("Failed to fetch devices: %s", error_text)
|
|
109
|
+
raise APIError(f"API error {resp.status}: {error_text}")
|
|
110
|
+
except aiohttp.ClientError as e:
|
|
111
|
+
_LOGGER.exception("Client error while fetching devices: %s", e)
|
|
112
|
+
raise APIError(f"Client error: {e}")
|
|
113
|
+
except Exception as e:
|
|
114
|
+
_LOGGER.exception("Exception while fetching devices: %s", e)
|
|
115
|
+
raise DayBetterError(f"Unexpected error: {e}")
|
|
116
|
+
|
|
117
|
+
async def fetch_pids(self) -> Dict[str, Any]:
|
|
118
|
+
"""Fetch device type PIDs.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Dictionary of device type PIDs
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
AuthenticationError: If authentication fails
|
|
125
|
+
APIError: If API request fails
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
session = self._get_session()
|
|
129
|
+
url = f"{self.base_url}hass/pids"
|
|
130
|
+
headers = self._get_headers()
|
|
131
|
+
|
|
132
|
+
async with session.post(url, headers=headers) as resp:
|
|
133
|
+
if resp.status == 200:
|
|
134
|
+
data = await resp.json()
|
|
135
|
+
self._auth_valid = True
|
|
136
|
+
return data.get("data", {})
|
|
137
|
+
elif resp.status == 401:
|
|
138
|
+
_LOGGER.error("Authentication failed - token may be expired")
|
|
139
|
+
self._auth_valid = False
|
|
140
|
+
raise AuthenticationError("Authentication failed - token may be expired")
|
|
141
|
+
else:
|
|
142
|
+
error_text = await resp.text()
|
|
143
|
+
_LOGGER.error("Failed to fetch PIDs: %s", error_text)
|
|
144
|
+
raise APIError(f"API error {resp.status}: {error_text}")
|
|
145
|
+
except aiohttp.ClientError as e:
|
|
146
|
+
_LOGGER.exception("Client error while fetching PIDs: %s", e)
|
|
147
|
+
raise APIError(f"Client error: {e}")
|
|
148
|
+
except Exception as e:
|
|
149
|
+
_LOGGER.exception("Exception while fetching PIDs: %s", e)
|
|
150
|
+
raise DayBetterError(f"Unexpected error: {e}")
|
|
151
|
+
|
|
152
|
+
async def control_device(
|
|
153
|
+
self,
|
|
154
|
+
device_name: str,
|
|
155
|
+
action: bool,
|
|
156
|
+
brightness: Optional[int] = None,
|
|
157
|
+
hs_color: Optional[Tuple[float, float]] = None,
|
|
158
|
+
color_temp: Optional[int] = None,
|
|
159
|
+
) -> Dict[str, Any]:
|
|
160
|
+
"""Control a device.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
device_name: Name of the device to control
|
|
164
|
+
action: Switch action (True/False)
|
|
165
|
+
brightness: Brightness value (0-255)
|
|
166
|
+
hs_color: Hue and saturation tuple (hue, saturation)
|
|
167
|
+
color_temp: Color temperature in mireds
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Control result dictionary
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
AuthenticationError: If authentication fails
|
|
174
|
+
APIError: If API request fails
|
|
175
|
+
"""
|
|
176
|
+
session = self._get_session()
|
|
177
|
+
url = f"{self.base_url}hass/control"
|
|
178
|
+
headers = self._get_headers()
|
|
179
|
+
|
|
180
|
+
# Priority: color temperature > color > brightness > switch
|
|
181
|
+
if color_temp is not None:
|
|
182
|
+
# Convert mireds to Kelvin
|
|
183
|
+
kelvin = int(1000000 / color_temp)
|
|
184
|
+
payload = {
|
|
185
|
+
"deviceName": device_name,
|
|
186
|
+
"type": 4, # Type 4 is color temperature control
|
|
187
|
+
"kelvin": kelvin,
|
|
188
|
+
}
|
|
189
|
+
elif hs_color is not None:
|
|
190
|
+
h, s = hs_color
|
|
191
|
+
v = (brightness / 255) if brightness is not None else 1.0
|
|
192
|
+
payload = {
|
|
193
|
+
"deviceName": device_name,
|
|
194
|
+
"type": 3,
|
|
195
|
+
"hue": h,
|
|
196
|
+
"saturation": s / 100,
|
|
197
|
+
"brightness": v,
|
|
198
|
+
}
|
|
199
|
+
elif brightness is not None:
|
|
200
|
+
payload = {
|
|
201
|
+
"deviceName": device_name,
|
|
202
|
+
"type": 2,
|
|
203
|
+
"brightness": brightness
|
|
204
|
+
}
|
|
205
|
+
else:
|
|
206
|
+
# Type 1 control switch is used by default
|
|
207
|
+
payload = {
|
|
208
|
+
"deviceName": device_name,
|
|
209
|
+
"type": 1,
|
|
210
|
+
"on": action
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
async with session.post(url, headers=headers, json=payload) as resp:
|
|
215
|
+
if resp.status == 200:
|
|
216
|
+
self._auth_valid = True
|
|
217
|
+
return await resp.json()
|
|
218
|
+
elif resp.status == 401:
|
|
219
|
+
_LOGGER.error("Authentication failed - token may be expired")
|
|
220
|
+
self._auth_valid = False
|
|
221
|
+
raise AuthenticationError("Authentication failed - token may be expired")
|
|
222
|
+
else:
|
|
223
|
+
error_text = await resp.text()
|
|
224
|
+
_LOGGER.error(
|
|
225
|
+
"Failed to control device %s: HTTP %d - %s",
|
|
226
|
+
device_name, resp.status, error_text
|
|
227
|
+
)
|
|
228
|
+
raise APIError(f"API error {resp.status}: {error_text}")
|
|
229
|
+
except aiohttp.ClientError as e:
|
|
230
|
+
_LOGGER.exception(
|
|
231
|
+
"Client error while controlling device %s: %s", device_name, e
|
|
232
|
+
)
|
|
233
|
+
raise APIError(f"Client error: {e}")
|
|
234
|
+
except Exception as e:
|
|
235
|
+
_LOGGER.exception(
|
|
236
|
+
"Exception while controlling device %s: %s", device_name, e
|
|
237
|
+
)
|
|
238
|
+
raise DayBetterError(f"Unexpected error: {e}")
|
|
239
|
+
|
|
240
|
+
async def fetch_mqtt_config(self) -> Dict[str, Any]:
|
|
241
|
+
"""Fetch MQTT connection configuration.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
MQTT configuration dictionary
|
|
245
|
+
|
|
246
|
+
Raises:
|
|
247
|
+
AuthenticationError: If authentication fails
|
|
248
|
+
APIError: If API request fails
|
|
249
|
+
"""
|
|
250
|
+
session = self._get_session()
|
|
251
|
+
url = f"{self.base_url}hass/cert"
|
|
252
|
+
headers = self._get_headers()
|
|
253
|
+
_LOGGER.debug("Requesting MQTT configuration URL: %s", url)
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
async with session.post(url, headers=headers) as resp:
|
|
257
|
+
_LOGGER.debug("MQTT configuration API response status: %d", resp.status)
|
|
258
|
+
|
|
259
|
+
if resp.status == 200:
|
|
260
|
+
data = await resp.json()
|
|
261
|
+
_LOGGER.debug("MQTT configuration API raw response: %s", data)
|
|
262
|
+
self._auth_valid = True
|
|
263
|
+
return data.get("data", {})
|
|
264
|
+
elif resp.status == 401:
|
|
265
|
+
_LOGGER.error("Authentication failed - token may be expired")
|
|
266
|
+
self._auth_valid = False
|
|
267
|
+
raise AuthenticationError("Authentication failed - token may be expired")
|
|
268
|
+
else:
|
|
269
|
+
error_text = await resp.text()
|
|
270
|
+
_LOGGER.error("Failed to fetch MQTT config: %s", error_text)
|
|
271
|
+
raise APIError(f"API error {resp.status}: {error_text}")
|
|
272
|
+
except aiohttp.ClientError as e:
|
|
273
|
+
_LOGGER.exception("Client error while fetching MQTT config: %s", e)
|
|
274
|
+
raise APIError(f"Client error: {e}")
|
|
275
|
+
except Exception as e:
|
|
276
|
+
_LOGGER.exception("Exception while fetching MQTT config: %s", e)
|
|
277
|
+
raise DayBetterError(f"Unexpected error: {e}")
|
|
278
|
+
|
|
279
|
+
async def fetch_device_statuses(self) -> List[Dict[str, Any]]:
|
|
280
|
+
"""Fetch statuses for all devices.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
List of device status dictionaries. Example item:
|
|
284
|
+
{
|
|
285
|
+
"deviceName": str,
|
|
286
|
+
"type": int,
|
|
287
|
+
"online": bool,
|
|
288
|
+
"temp": int,
|
|
289
|
+
"humi": int,
|
|
290
|
+
"bettery": int
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
Raises:
|
|
294
|
+
AuthenticationError: If authentication fails
|
|
295
|
+
APIError: If API request fails
|
|
296
|
+
"""
|
|
297
|
+
try:
|
|
298
|
+
session = self._get_session()
|
|
299
|
+
url = f"{self.base_url}hass/status"
|
|
300
|
+
headers = self._get_headers()
|
|
301
|
+
|
|
302
|
+
async with session.post(url, headers=headers) as resp:
|
|
303
|
+
if resp.status == 200:
|
|
304
|
+
data = await resp.json()
|
|
305
|
+
self._auth_valid = True
|
|
306
|
+
# API expected to return { "data": [...] }
|
|
307
|
+
return data.get("data", [])
|
|
308
|
+
elif resp.status == 401:
|
|
309
|
+
_LOGGER.error("Authentication failed - token may be expired")
|
|
310
|
+
self._auth_valid = False
|
|
311
|
+
raise AuthenticationError("Authentication failed - token may be expired")
|
|
312
|
+
else:
|
|
313
|
+
error_text = await resp.text()
|
|
314
|
+
_LOGGER.error("Failed to fetch device statuses: %s", error_text)
|
|
315
|
+
raise APIError(f"API error {resp.status}: {error_text}")
|
|
316
|
+
except aiohttp.ClientError as e:
|
|
317
|
+
_LOGGER.exception("Client error while fetching device statuses: %s", e)
|
|
318
|
+
raise APIError(f"Client error: {e}")
|
|
319
|
+
except Exception as e:
|
|
320
|
+
_LOGGER.exception("Exception while fetching device statuses: %s", e)
|
|
321
|
+
raise DayBetterError(f"Unexpected error: {e}")
|
|
322
|
+
|
|
323
|
+
async def integrate(self, hass_code: str) -> Dict[str, Any]:
|
|
324
|
+
"""Integrate with Home Assistant using hassCode.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
hass_code: Home Assistant integration code from APP
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Integration result dictionary
|
|
331
|
+
|
|
332
|
+
Raises:
|
|
333
|
+
APIError: If API request fails
|
|
334
|
+
"""
|
|
335
|
+
# 根据 hass_code 动态更新 base_url(如果之前没有明确指定)
|
|
336
|
+
# 如果 hass_code 以 "db-" 开头,切换到正式环境
|
|
337
|
+
if hass_code.startswith("db-") and self.base_url != self.PROD_BASE_URL:
|
|
338
|
+
old_url = self.base_url
|
|
339
|
+
self.base_url = self.PROD_BASE_URL
|
|
340
|
+
_LOGGER.info(
|
|
341
|
+
"Switching to production environment based on hass_code. "
|
|
342
|
+
"URL changed from %s to %s", old_url, self.base_url
|
|
343
|
+
)
|
|
344
|
+
elif not hass_code.startswith("db-") and self.base_url != self.TEST_BASE_URL:
|
|
345
|
+
# 如果 hass_code 不以 "db-" 开头,且当前不是测试环境,切换到测试环境
|
|
346
|
+
old_url = self.base_url
|
|
347
|
+
self.base_url = self.TEST_BASE_URL
|
|
348
|
+
_LOGGER.info(
|
|
349
|
+
"Switching to test environment based on hass_code. "
|
|
350
|
+
"URL changed from %s to %s", old_url, self.base_url
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
session = self._get_session()
|
|
355
|
+
url = f"{self.base_url}hass/integrate"
|
|
356
|
+
payload = {"hassCode": hass_code}
|
|
357
|
+
|
|
358
|
+
async with session.post(url, json=payload) as resp:
|
|
359
|
+
if resp.status == 200:
|
|
360
|
+
data = await resp.json()
|
|
361
|
+
_LOGGER.debug("Integration successful: %s", data)
|
|
362
|
+
return data
|
|
363
|
+
else:
|
|
364
|
+
error_text = await resp.text()
|
|
365
|
+
_LOGGER.error("Failed to integrate: %s", error_text)
|
|
366
|
+
raise APIError(f"API error {resp.status}: {error_text}")
|
|
367
|
+
except aiohttp.ClientError as e:
|
|
368
|
+
_LOGGER.exception("Client error while integrating: %s", e)
|
|
369
|
+
raise APIError(f"Client error: {e}")
|
|
370
|
+
except Exception as e:
|
|
371
|
+
_LOGGER.exception("Exception while integrating: %s", e)
|
|
372
|
+
raise DayBetterError(f"Unexpected error: {e}")
|
|
373
|
+
|
|
374
|
+
@property
|
|
375
|
+
def is_authenticated(self) -> bool:
|
|
376
|
+
"""Check if the API client is authenticated."""
|
|
377
|
+
return self._auth_valid
|
|
378
|
+
|
|
379
|
+
def filter_sensor_devices(
|
|
380
|
+
self,
|
|
381
|
+
devices: List[Dict[str, Any]],
|
|
382
|
+
pids: Dict[str, Any],
|
|
383
|
+
) -> List[Dict[str, Any]]:
|
|
384
|
+
"""Filter devices to only include sensors based on PID.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
devices: List of all devices
|
|
388
|
+
pids: Dictionary containing device type PIDs
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
List of sensor devices only
|
|
392
|
+
"""
|
|
393
|
+
sensor_pids_str = pids.get("sensor", "")
|
|
394
|
+
if not sensor_pids_str:
|
|
395
|
+
return []
|
|
396
|
+
|
|
397
|
+
sensor_pids = {pid.strip() for pid in sensor_pids_str.split(",")}
|
|
398
|
+
|
|
399
|
+
return [
|
|
400
|
+
device
|
|
401
|
+
for device in devices
|
|
402
|
+
if device.get("deviceMoldPid", "") in sensor_pids
|
|
403
|
+
]
|
|
404
|
+
|
|
405
|
+
def merge_device_status(
|
|
406
|
+
self,
|
|
407
|
+
devices: List[Dict[str, Any]],
|
|
408
|
+
statuses: List[Dict[str, Any]],
|
|
409
|
+
) -> List[Dict[str, Any]]:
|
|
410
|
+
"""Merge device info with status info.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
devices: List of device info dictionaries
|
|
414
|
+
statuses: List of device status dictionaries
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
List of merged device dictionaries
|
|
418
|
+
"""
|
|
419
|
+
status_dict = {status.get("deviceName"): status for status in statuses}
|
|
420
|
+
|
|
421
|
+
merged = []
|
|
422
|
+
for device in devices:
|
|
423
|
+
device_name = device.get("deviceName")
|
|
424
|
+
merged_device = device.copy()
|
|
425
|
+
|
|
426
|
+
if device_name in status_dict:
|
|
427
|
+
merged_device.update(status_dict[device_name])
|
|
428
|
+
|
|
429
|
+
merged.append(merged_device)
|
|
430
|
+
|
|
431
|
+
return merged
|
|
432
|
+
|
|
433
|
+
async def fetch_sensor_data(self) -> List[Dict[str, Any]]:
|
|
434
|
+
"""Fetch and process sensor data in one call.
|
|
435
|
+
|
|
436
|
+
This method fetches device statuses, devices list, and PIDs,
|
|
437
|
+
filters for sensor devices, and merges the data.
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
List of sensor devices with merged status data
|
|
441
|
+
|
|
442
|
+
Raises:
|
|
443
|
+
AuthenticationError: If authentication fails
|
|
444
|
+
APIError: If API request fails
|
|
445
|
+
"""
|
|
446
|
+
# Fetch current statuses
|
|
447
|
+
statuses = await self.fetch_device_statuses()
|
|
448
|
+
|
|
449
|
+
# Fetch devices and PIDs if not cached
|
|
450
|
+
if not self._devices or not self._pids:
|
|
451
|
+
self._devices = await self.fetch_devices()
|
|
452
|
+
self._pids = await self.fetch_pids()
|
|
453
|
+
|
|
454
|
+
# Filter to sensor devices only
|
|
455
|
+
sensor_devices = self.filter_sensor_devices(self._devices, self._pids)
|
|
456
|
+
|
|
457
|
+
# Merge with current status
|
|
458
|
+
merged = self.merge_device_status(sensor_devices, statuses)
|
|
459
|
+
_LOGGER.debug("Fetched %d sensor devices", len(merged))
|
|
460
|
+
return merged
|
|
461
|
+
|
|
462
|
+
async def close(self) -> None:
|
|
463
|
+
"""Close the client session."""
|
|
464
|
+
if self._session:
|
|
465
|
+
await self._session.close()
|
|
466
|
+
self._session = None
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""DayBetter client exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DayBetterError(Exception):
|
|
5
|
+
"""Base exception for DayBetter client."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str) -> None:
|
|
8
|
+
"""Initialize the exception.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
message: Error message
|
|
12
|
+
"""
|
|
13
|
+
super().__init__(message)
|
|
14
|
+
self.message = message
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AuthenticationError(DayBetterError):
|
|
18
|
+
"""Authentication failed."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, message: str = "Authentication failed") -> None:
|
|
21
|
+
"""Initialize the authentication error.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
message: Error message
|
|
25
|
+
"""
|
|
26
|
+
super().__init__(message)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class APIError(DayBetterError):
|
|
30
|
+
"""API request failed."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, message: str) -> None:
|
|
33
|
+
"""Initialize the API error.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
message: Error message
|
|
37
|
+
"""
|
|
38
|
+
super().__init__(message)
|
|
File without changes
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: daybetter-services-python
|
|
3
|
+
Version: 1.0.7
|
|
4
|
+
Summary: Python client for DayBetter devices and services
|
|
5
|
+
Home-page: https://github.com/THDayBetter/daybetter-python
|
|
6
|
+
Author: THDayBetter
|
|
7
|
+
Author-email: THDayBetter <chenp2368@163.com>
|
|
8
|
+
Maintainer-email: THDayBetter <chenp2368@163.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Project-URL: Homepage, https://github.com/THDayBetter/daybetter-python
|
|
11
|
+
Project-URL: Documentation, https://github.com/THDayBetter/daybetter-python#readme
|
|
12
|
+
Project-URL: Repository, https://github.com/THDayBetter/daybetter-python.git
|
|
13
|
+
Project-URL: Bug Tracker, https://github.com/THDayBetter/daybetter-python/issues
|
|
14
|
+
Keywords: daybetter,iot,home automation,mqtt
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: aiohttp>=3.8.0
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
32
|
+
Requires-Dist: black; extra == "dev"
|
|
33
|
+
Requires-Dist: isort; extra == "dev"
|
|
34
|
+
Requires-Dist: flake8; extra == "dev"
|
|
35
|
+
Requires-Dist: mypy; extra == "dev"
|
|
36
|
+
Dynamic: author
|
|
37
|
+
Dynamic: home-page
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
Dynamic: requires-python
|
|
40
|
+
|
|
41
|
+
# DayBetter Python Client
|
|
42
|
+
|
|
43
|
+
A Python client library for interacting with DayBetter devices and services.
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- Device management and control
|
|
48
|
+
- MQTT configuration retrieval
|
|
49
|
+
- Authentication handling
|
|
50
|
+
- Async/await support
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install daybetter-services-python
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import asyncio
|
|
62
|
+
from daybetter_python import DayBetterClient
|
|
63
|
+
|
|
64
|
+
async def main():
|
|
65
|
+
async with DayBetterClient(token="your_token") as client:
|
|
66
|
+
# Fetch devices
|
|
67
|
+
devices = await client.fetch_devices()
|
|
68
|
+
print(f"Found {len(devices)} devices")
|
|
69
|
+
|
|
70
|
+
# Control a device
|
|
71
|
+
result = await client.control_device(
|
|
72
|
+
device_name="device_001",
|
|
73
|
+
action=True,
|
|
74
|
+
brightness=80
|
|
75
|
+
)
|
|
76
|
+
print(f"Control result: {result}")
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
asyncio.run(main())
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## API Reference
|
|
83
|
+
|
|
84
|
+
### DayBetterClient
|
|
85
|
+
|
|
86
|
+
#### Methods
|
|
87
|
+
|
|
88
|
+
- `fetch_devices()`: Get list of devices
|
|
89
|
+
- `fetch_pids()`: Get device type PIDs
|
|
90
|
+
- `control_device(device_name, action, brightness, hs_color, color_temp)`: Control a device
|
|
91
|
+
- `fetch_mqtt_config()`: Get MQTT configuration
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
MIT License
|
|
96
|
+
|
|
97
|
+
## Contributing
|
|
98
|
+
|
|
99
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
daybetter_python/__init__.py,sha256=xLxgdbAff9zysqqwgS3FQ8Ba0b4tOi0pKWyjF6MyTO8,252
|
|
2
|
+
daybetter_python/client.py,sha256=ayAHorsnvGeQ1PWGRP-OD9WK0OP6DHQ6rxcq0h3_BMw,18009
|
|
3
|
+
daybetter_python/exceptions.py,sha256=MGZdRDe711BOCF2bE8Oc7Gph-QXx7HbszOUsbPYuUjQ,903
|
|
4
|
+
daybetter_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
daybetter_services_python-1.0.7.dist-info/licenses/LICENSE,sha256=DbDD4ZqrGs-Zj3ffqcf35iwo9gM2bBc4WSCYbohM1-M,1068
|
|
6
|
+
daybetter_services_python-1.0.7.dist-info/METADATA,sha256=0HDBAzHqnrnUy_xeOPPhGMBOr9mR6EiS8CinO8Uhnio,2845
|
|
7
|
+
daybetter_services_python-1.0.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
daybetter_services_python-1.0.7.dist-info/top_level.txt,sha256=cRqnqd_T8N2nPkR6x6fqB-DkUgXQ0baNqayVn6RdDFo,17
|
|
9
|
+
daybetter_services_python-1.0.7.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 THDayBetter
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
daybetter_python
|