daybetter-services-python 1.0.6__tar.gz → 1.0.8__tar.gz

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.
Files changed (20) hide show
  1. daybetter_services_python-1.0.8/PKG-INFO +127 -0
  2. daybetter_services_python-1.0.8/README.md +92 -0
  3. {daybetter_services_python-1.0.6 → daybetter_services_python-1.0.8}/daybetter_python/__init__.py +1 -1
  4. {daybetter_services_python-1.0.6 → daybetter_services_python-1.0.8}/daybetter_python/client.py +86 -92
  5. daybetter_services_python-1.0.8/daybetter_python/exceptions.py +38 -0
  6. daybetter_services_python-1.0.8/daybetter_python/py.typed +0 -0
  7. daybetter_services_python-1.0.8/daybetter_services_python.egg-info/PKG-INFO +127 -0
  8. {daybetter_services_python-1.0.6 → daybetter_services_python-1.0.8}/daybetter_services_python.egg-info/SOURCES.txt +3 -2
  9. {daybetter_services_python-1.0.6 → daybetter_services_python-1.0.8}/pyproject.toml +9 -1
  10. daybetter_services_python-1.0.8/tests/test_placeholder.py +7 -0
  11. daybetter_services_python-1.0.6/PKG-INFO +0 -98
  12. daybetter_services_python-1.0.6/README.md +0 -59
  13. daybetter_services_python-1.0.6/daybetter_python/exceptions.py +0 -13
  14. daybetter_services_python-1.0.6/daybetter_services_python.egg-info/PKG-INFO +0 -98
  15. daybetter_services_python-1.0.6/setup.py +0 -34
  16. {daybetter_services_python-1.0.6 → daybetter_services_python-1.0.8}/LICENSE +0 -0
  17. {daybetter_services_python-1.0.6 → daybetter_services_python-1.0.8}/daybetter_services_python.egg-info/dependency_links.txt +0 -0
  18. {daybetter_services_python-1.0.6 → daybetter_services_python-1.0.8}/daybetter_services_python.egg-info/requires.txt +0 -0
  19. {daybetter_services_python-1.0.6 → daybetter_services_python-1.0.8}/daybetter_services_python.egg-info/top_level.txt +0 -0
  20. {daybetter_services_python-1.0.6 → daybetter_services_python-1.0.8}/setup.cfg +0 -0
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: daybetter-services-python
3
+ Version: 1.0.8
4
+ Summary: Python client for DayBetter devices and services
5
+ Author-email: THDayBetter <chenp2368@163.com>
6
+ Maintainer-email: THDayBetter <chenp2368@163.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/THDayBetter/daybetter-python
9
+ Project-URL: Documentation, https://github.com/THDayBetter/daybetter-python#readme
10
+ Project-URL: Repository, https://github.com/THDayBetter/daybetter-python.git
11
+ Project-URL: Bug Tracker, https://github.com/THDayBetter/daybetter-python/issues
12
+ Keywords: daybetter,iot,home automation,mqtt
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.8
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: aiohttp>=3.8.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=6.0; extra == "dev"
29
+ Requires-Dist: pytest-asyncio; extra == "dev"
30
+ Requires-Dist: black; extra == "dev"
31
+ Requires-Dist: isort; extra == "dev"
32
+ Requires-Dist: flake8; extra == "dev"
33
+ Requires-Dist: mypy; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # DayBetter Services Python Client
37
+
38
+ [![PyPI](https://img.shields.io/pypi/v/daybetter-services-python.svg)](https://pypi.org/project/daybetter-services-python/)
39
+ [![Python Versions](https://img.shields.io/pypi/pyversions/daybetter-services-python.svg)](https://pypi.org/project/daybetter-services-python/)
40
+ [![License](https://img.shields.io/pypi/l/daybetter-services-python.svg)](LICENSE)
41
+
42
+ Asynchronous client library used by the [Home Assistant DayBetter Services integration](https://github.com/home-assistant/core/pull/154677).
43
+ It handles authentication, device discovery, status polling, PID metadata and device control against the DayBetter cloud API.
44
+
45
+ ## Features
46
+
47
+ - Fully async `DayBetterClient` with context-manager support
48
+ - Automatic environment selection (test/prod) based on `hass_code`
49
+ - Convenience helpers for common API endpoints (devices, statuses, MQTT config)
50
+ - Sensor-focused workflow `fetch_sensor_data()` that merges devices, statuses and PID filters
51
+ - Type hints + `py.typed` for first-class editor / mypy support
52
+ - Published on PyPI for Home Assistant and other integrations
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ pip install daybetter-services-python
58
+ ```
59
+
60
+ ## Quick Start
61
+
62
+ ```python
63
+ import asyncio
64
+ from daybetter_python import DayBetterClient
65
+
66
+ async def main() -> None:
67
+ async with DayBetterClient(token="YOUR_TOKEN", hass_code="db-xxxx") as client:
68
+ # Fetch merged sensor payloads (used by Home Assistant)
69
+ sensors = await client.fetch_sensor_data()
70
+ for item in sensors:
71
+ print(item["deviceName"], item.get("temp"), item.get("humi"))
72
+
73
+ # Control a device (brightness example)
74
+ await client.control_device(
75
+ device_name="device_001",
76
+ action=True,
77
+ brightness=180,
78
+ )
79
+
80
+ if __name__ == "__main__":
81
+ asyncio.run(main())
82
+ ```
83
+
84
+ ## Home Assistant Integration
85
+
86
+ The official integration PR can be followed here: [home-assistant/core#154677](https://github.com/home-assistant/core/pull/154677).
87
+ The integration imports this library and simply calls `client.fetch_sensor_data()` inside a data coordinator, so all business logic lives in this package.
88
+
89
+ See `docs/homeassistant.md` for:
90
+ - Installation instructions (pip, custom component)
91
+ - How to obtain the DayBetter token and `hass_code`
92
+ - Sample `configuration.yaml` snippets
93
+ - Known limitations and future roadmap
94
+
95
+ ## Development
96
+
97
+ ```bash
98
+ git clone https://github.com/THDayBetter/daybetter-services-python.git
99
+ cd daybetter-services-python
100
+ python3 -m venv .venv
101
+ source .venv/bin/activate
102
+ pip install -e .[dev]
103
+
104
+ # Quality checks
105
+ flake8 daybetter_python
106
+ mypy daybetter_python
107
+ pytest
108
+ ```
109
+
110
+ Pull requests are welcome! Please open an issue if you encounter problems with the DayBetter cloud API.
111
+
112
+ ## Release Process
113
+
114
+ 1. Update `pyproject.toml` and `daybetter_python/__init__.py` with the new version.
115
+ 2. Document changes in `CHANGELOG.md`.
116
+ 3. Build and upload:
117
+ ```bash
118
+ rm -rf dist build *.egg-info
119
+ python -m build
120
+ twine check dist/*
121
+ twine upload dist/*
122
+ ```
123
+ 4. Create a Git tag (e.g., `git tag v1.0.7 && git push origin v1.0.7`).
124
+
125
+ ## License
126
+
127
+ This project is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,92 @@
1
+ # DayBetter Services Python Client
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/daybetter-services-python.svg)](https://pypi.org/project/daybetter-services-python/)
4
+ [![Python Versions](https://img.shields.io/pypi/pyversions/daybetter-services-python.svg)](https://pypi.org/project/daybetter-services-python/)
5
+ [![License](https://img.shields.io/pypi/l/daybetter-services-python.svg)](LICENSE)
6
+
7
+ Asynchronous client library used by the [Home Assistant DayBetter Services integration](https://github.com/home-assistant/core/pull/154677).
8
+ It handles authentication, device discovery, status polling, PID metadata and device control against the DayBetter cloud API.
9
+
10
+ ## Features
11
+
12
+ - Fully async `DayBetterClient` with context-manager support
13
+ - Automatic environment selection (test/prod) based on `hass_code`
14
+ - Convenience helpers for common API endpoints (devices, statuses, MQTT config)
15
+ - Sensor-focused workflow `fetch_sensor_data()` that merges devices, statuses and PID filters
16
+ - Type hints + `py.typed` for first-class editor / mypy support
17
+ - Published on PyPI for Home Assistant and other integrations
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install daybetter-services-python
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```python
28
+ import asyncio
29
+ from daybetter_python import DayBetterClient
30
+
31
+ async def main() -> None:
32
+ async with DayBetterClient(token="YOUR_TOKEN", hass_code="db-xxxx") as client:
33
+ # Fetch merged sensor payloads (used by Home Assistant)
34
+ sensors = await client.fetch_sensor_data()
35
+ for item in sensors:
36
+ print(item["deviceName"], item.get("temp"), item.get("humi"))
37
+
38
+ # Control a device (brightness example)
39
+ await client.control_device(
40
+ device_name="device_001",
41
+ action=True,
42
+ brightness=180,
43
+ )
44
+
45
+ if __name__ == "__main__":
46
+ asyncio.run(main())
47
+ ```
48
+
49
+ ## Home Assistant Integration
50
+
51
+ The official integration PR can be followed here: [home-assistant/core#154677](https://github.com/home-assistant/core/pull/154677).
52
+ The integration imports this library and simply calls `client.fetch_sensor_data()` inside a data coordinator, so all business logic lives in this package.
53
+
54
+ See `docs/homeassistant.md` for:
55
+ - Installation instructions (pip, custom component)
56
+ - How to obtain the DayBetter token and `hass_code`
57
+ - Sample `configuration.yaml` snippets
58
+ - Known limitations and future roadmap
59
+
60
+ ## Development
61
+
62
+ ```bash
63
+ git clone https://github.com/THDayBetter/daybetter-services-python.git
64
+ cd daybetter-services-python
65
+ python3 -m venv .venv
66
+ source .venv/bin/activate
67
+ pip install -e .[dev]
68
+
69
+ # Quality checks
70
+ flake8 daybetter_python
71
+ mypy daybetter_python
72
+ pytest
73
+ ```
74
+
75
+ Pull requests are welcome! Please open an issue if you encounter problems with the DayBetter cloud API.
76
+
77
+ ## Release Process
78
+
79
+ 1. Update `pyproject.toml` and `daybetter_python/__init__.py` with the new version.
80
+ 2. Document changes in `CHANGELOG.md`.
81
+ 3. Build and upload:
82
+ ```bash
83
+ rm -rf dist build *.egg-info
84
+ python -m build
85
+ twine check dist/*
86
+ twine upload dist/*
87
+ ```
88
+ 4. Create a Git tag (e.g., `git tag v1.0.7 && git push origin v1.0.7`).
89
+
90
+ ## License
91
+
92
+ This project is licensed under the [MIT License](LICENSE).
@@ -3,5 +3,5 @@
3
3
  from .client import DayBetterClient
4
4
  from .exceptions import DayBetterError, AuthenticationError, APIError
5
5
 
6
- __version__ = "1.0.6"
6
+ __version__ = "1.0.8"
7
7
  __all__ = ["DayBetterClient", "DayBetterError", "AuthenticationError", "APIError"]
@@ -1,83 +1,88 @@
1
1
  """DayBetter API client."""
2
2
 
3
- import aiohttp
4
3
  import logging
5
- from typing import Any, Dict, List, Optional, Tuple
4
+ from typing import Any, Dict, List, Optional, Tuple, Type
5
+
6
+ import aiohttp
6
7
 
7
- from .exceptions import DayBetterError, AuthenticationError, APIError
8
+ from .exceptions import APIError, AuthenticationError, DayBetterError
8
9
 
9
10
  _LOGGER = logging.getLogger(__name__)
10
11
 
11
12
 
12
13
  class DayBetterClient:
13
14
  """DayBetter API client."""
14
-
15
- # 测试环境URL
15
+
16
16
  TEST_BASE_URL = "https://cloud.v2.dbiot.link/daybetter/hass/api/v1.0/"
17
- # 正式环境URL
18
17
  PROD_BASE_URL = "https://a.dbiot.org/daybetter/hass/api/v1.0/"
19
-
18
+
20
19
  def __init__(
21
- self,
22
- token: str,
20
+ self,
21
+ token: str,
23
22
  base_url: Optional[str] = None,
24
- hass_code: Optional[str] = None
23
+ hass_code: Optional[str] = None,
24
+ session: Optional[aiohttp.ClientSession] = None,
25
25
  ):
26
26
  """Initialize the client.
27
-
27
+
28
28
  Args:
29
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)
30
+ base_url: Base URL for the API (optional, determined by hass_code if not provided)
31
+ hass_code: Home Assistant integration code (optional, if provided and starts
32
+ with "db-", will use production environment)
33
+ session: Optional aiohttp ClientSession. If provided, the client will use it
34
+ and will not close it when close() is called (caller owns the session).
33
35
  """
34
36
  self.token = token
35
-
36
- # 根据 hass_code 或 base_url 确定使用的环境
37
+
37
38
  if base_url is not None:
38
- # 如果明确指定了 base_url,使用指定的 URL
39
39
  self.base_url = base_url
40
40
  elif hass_code is not None and hass_code.startswith("db-"):
41
- # 如果 hass_code 以 "db-" 开头,使用正式环境
42
41
  self.base_url = self.PROD_BASE_URL
43
42
  _LOGGER.debug("Using production environment based on hass_code")
44
43
  else:
45
- # 默认使用测试环境
46
44
  self.base_url = self.TEST_BASE_URL
47
45
  _LOGGER.debug("Using test environment")
48
-
49
- self._session: Optional[aiohttp.ClientSession] = None
46
+
47
+ self._session: Optional[aiohttp.ClientSession] = session
48
+ self._own_session: bool = session is None
50
49
  self._auth_valid = True
51
50
  self._devices: List[Dict[str, Any]] = []
52
51
  self._pids: Dict[str, Any] = {}
53
-
52
+
54
53
  async def __aenter__(self):
55
54
  """Async context manager entry."""
56
- self._session = aiohttp.ClientSession()
55
+ if self._session is None:
56
+ self._session = aiohttp.ClientSession()
57
57
  return self
58
-
59
- async def __aexit__(self, exc_type, exc_val, exc_tb):
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:
60
65
  """Async context manager exit."""
61
- if self._session:
66
+ if self._own_session and self._session is not None:
62
67
  await self._session.close()
63
68
  self._session = None
64
-
69
+
65
70
  def _get_session(self) -> aiohttp.ClientSession:
66
71
  """Get or create aiohttp session."""
67
- if not self._session:
72
+ if self._session is None:
68
73
  self._session = aiohttp.ClientSession()
69
74
  return self._session
70
-
75
+
71
76
  def _get_headers(self) -> Dict[str, str]:
72
77
  """Get request headers."""
73
78
  return {"Authorization": f"Bearer {self.token}"}
74
-
79
+
75
80
  async def fetch_devices(self) -> List[Dict[str, Any]]:
76
81
  """Fetch devices from API.
77
-
82
+
78
83
  Returns:
79
84
  List of device dictionaries
80
-
85
+
81
86
  Raises:
82
87
  AuthenticationError: If authentication fails
83
88
  APIError: If API request fails
@@ -86,7 +91,7 @@ class DayBetterClient:
86
91
  session = self._get_session()
87
92
  url = f"{self.base_url}hass/devices"
88
93
  headers = self._get_headers()
89
-
94
+
90
95
  async with session.post(url, headers=headers) as resp:
91
96
  if resp.status == 200:
92
97
  data = await resp.json()
@@ -108,13 +113,13 @@ class DayBetterClient:
108
113
  except Exception as e:
109
114
  _LOGGER.exception("Exception while fetching devices: %s", e)
110
115
  raise DayBetterError(f"Unexpected error: {e}")
111
-
116
+
112
117
  async def fetch_pids(self) -> Dict[str, Any]:
113
118
  """Fetch device type PIDs.
114
-
119
+
115
120
  Returns:
116
121
  Dictionary of device type PIDs
117
-
122
+
118
123
  Raises:
119
124
  AuthenticationError: If authentication fails
120
125
  APIError: If API request fails
@@ -123,7 +128,7 @@ class DayBetterClient:
123
128
  session = self._get_session()
124
129
  url = f"{self.base_url}hass/pids"
125
130
  headers = self._get_headers()
126
-
131
+
127
132
  async with session.post(url, headers=headers) as resp:
128
133
  if resp.status == 200:
129
134
  data = await resp.json()
@@ -143,7 +148,7 @@ class DayBetterClient:
143
148
  except Exception as e:
144
149
  _LOGGER.exception("Exception while fetching PIDs: %s", e)
145
150
  raise DayBetterError(f"Unexpected error: {e}")
146
-
151
+
147
152
  async def control_device(
148
153
  self,
149
154
  device_name: str,
@@ -153,17 +158,17 @@ class DayBetterClient:
153
158
  color_temp: Optional[int] = None,
154
159
  ) -> Dict[str, Any]:
155
160
  """Control a device.
156
-
161
+
157
162
  Args:
158
163
  device_name: Name of the device to control
159
164
  action: Switch action (True/False)
160
165
  brightness: Brightness value (0-255)
161
166
  hs_color: Hue and saturation tuple (hue, saturation)
162
167
  color_temp: Color temperature in mireds
163
-
168
+
164
169
  Returns:
165
170
  Control result dictionary
166
-
171
+
167
172
  Raises:
168
173
  AuthenticationError: If authentication fails
169
174
  APIError: If API request fails
@@ -171,10 +176,8 @@ class DayBetterClient:
171
176
  session = self._get_session()
172
177
  url = f"{self.base_url}hass/control"
173
178
  headers = self._get_headers()
174
-
175
- # Priority: color temperature > color > brightness > switch
179
+
176
180
  if color_temp is not None:
177
- # Convert mireds to Kelvin
178
181
  kelvin = int(1000000 / color_temp)
179
182
  payload = {
180
183
  "deviceName": device_name,
@@ -193,18 +196,17 @@ class DayBetterClient:
193
196
  }
194
197
  elif brightness is not None:
195
198
  payload = {
196
- "deviceName": device_name,
197
- "type": 2,
199
+ "deviceName": device_name,
200
+ "type": 2,
198
201
  "brightness": brightness
199
202
  }
200
203
  else:
201
- # Type 1 control switch is used by default
202
204
  payload = {
203
- "deviceName": device_name,
204
- "type": 1,
205
+ "deviceName": device_name,
206
+ "type": 1,
205
207
  "on": action
206
208
  }
207
-
209
+
208
210
  try:
209
211
  async with session.post(url, headers=headers, json=payload) as resp:
210
212
  if resp.status == 200:
@@ -217,7 +219,7 @@ class DayBetterClient:
217
219
  else:
218
220
  error_text = await resp.text()
219
221
  _LOGGER.error(
220
- "Failed to control device %s: HTTP %d - %s",
222
+ "Failed to control device %s: HTTP %d - %s",
221
223
  device_name, resp.status, error_text
222
224
  )
223
225
  raise APIError(f"API error {resp.status}: {error_text}")
@@ -231,13 +233,13 @@ class DayBetterClient:
231
233
  "Exception while controlling device %s: %s", device_name, e
232
234
  )
233
235
  raise DayBetterError(f"Unexpected error: {e}")
234
-
236
+
235
237
  async def fetch_mqtt_config(self) -> Dict[str, Any]:
236
238
  """Fetch MQTT connection configuration.
237
-
239
+
238
240
  Returns:
239
241
  MQTT configuration dictionary
240
-
242
+
241
243
  Raises:
242
244
  AuthenticationError: If authentication fails
243
245
  APIError: If API request fails
@@ -246,11 +248,11 @@ class DayBetterClient:
246
248
  url = f"{self.base_url}hass/cert"
247
249
  headers = self._get_headers()
248
250
  _LOGGER.debug("Requesting MQTT configuration URL: %s", url)
249
-
251
+
250
252
  try:
251
253
  async with session.post(url, headers=headers) as resp:
252
254
  _LOGGER.debug("MQTT configuration API response status: %d", resp.status)
253
-
255
+
254
256
  if resp.status == 200:
255
257
  data = await resp.json()
256
258
  _LOGGER.debug("MQTT configuration API raw response: %s", data)
@@ -270,10 +272,10 @@ class DayBetterClient:
270
272
  except Exception as e:
271
273
  _LOGGER.exception("Exception while fetching MQTT config: %s", e)
272
274
  raise DayBetterError(f"Unexpected error: {e}")
273
-
275
+
274
276
  async def fetch_device_statuses(self) -> List[Dict[str, Any]]:
275
277
  """Fetch statuses for all devices.
276
-
278
+
277
279
  Returns:
278
280
  List of device status dictionaries. Example item:
279
281
  {
@@ -284,7 +286,7 @@ class DayBetterClient:
284
286
  "humi": int,
285
287
  "bettery": int
286
288
  }
287
-
289
+
288
290
  Raises:
289
291
  AuthenticationError: If authentication fails
290
292
  APIError: If API request fails
@@ -293,12 +295,11 @@ class DayBetterClient:
293
295
  session = self._get_session()
294
296
  url = f"{self.base_url}hass/status"
295
297
  headers = self._get_headers()
296
-
298
+
297
299
  async with session.post(url, headers=headers) as resp:
298
300
  if resp.status == 200:
299
301
  data = await resp.json()
300
302
  self._auth_valid = True
301
- # API expected to return { "data": [...] }
302
303
  return data.get("data", [])
303
304
  elif resp.status == 401:
304
305
  _LOGGER.error("Authentication failed - token may be expired")
@@ -314,21 +315,19 @@ class DayBetterClient:
314
315
  except Exception as e:
315
316
  _LOGGER.exception("Exception while fetching device statuses: %s", e)
316
317
  raise DayBetterError(f"Unexpected error: {e}")
317
-
318
+
318
319
  async def integrate(self, hass_code: str) -> Dict[str, Any]:
319
320
  """Integrate with Home Assistant using hassCode.
320
-
321
+
321
322
  Args:
322
323
  hass_code: Home Assistant integration code from APP
323
-
324
+
324
325
  Returns:
325
326
  Integration result dictionary
326
-
327
+
327
328
  Raises:
328
329
  APIError: If API request fails
329
330
  """
330
- # 根据 hass_code 动态更新 base_url(如果之前没有明确指定)
331
- # 如果 hass_code 以 "db-" 开头,切换到正式环境
332
331
  if hass_code.startswith("db-") and self.base_url != self.PROD_BASE_URL:
333
332
  old_url = self.base_url
334
333
  self.base_url = self.PROD_BASE_URL
@@ -337,19 +336,18 @@ class DayBetterClient:
337
336
  "URL changed from %s to %s", old_url, self.base_url
338
337
  )
339
338
  elif not hass_code.startswith("db-") and self.base_url != self.TEST_BASE_URL:
340
- # 如果 hass_code 不以 "db-" 开头,且当前不是测试环境,切换到测试环境
341
339
  old_url = self.base_url
342
340
  self.base_url = self.TEST_BASE_URL
343
341
  _LOGGER.info(
344
342
  "Switching to test environment based on hass_code. "
345
343
  "URL changed from %s to %s", old_url, self.base_url
346
344
  )
347
-
345
+
348
346
  try:
349
347
  session = self._get_session()
350
348
  url = f"{self.base_url}hass/integrate"
351
349
  payload = {"hassCode": hass_code}
352
-
350
+
353
351
  async with session.post(url, json=payload) as resp:
354
352
  if resp.status == 200:
355
353
  data = await resp.json()
@@ -365,23 +363,23 @@ class DayBetterClient:
365
363
  except Exception as e:
366
364
  _LOGGER.exception("Exception while integrating: %s", e)
367
365
  raise DayBetterError(f"Unexpected error: {e}")
368
-
366
+
369
367
  @property
370
368
  def is_authenticated(self) -> bool:
371
369
  """Check if the API client is authenticated."""
372
370
  return self._auth_valid
373
-
371
+
374
372
  def filter_sensor_devices(
375
373
  self,
376
374
  devices: List[Dict[str, Any]],
377
375
  pids: Dict[str, Any],
378
376
  ) -> List[Dict[str, Any]]:
379
377
  """Filter devices to only include sensors based on PID.
380
-
378
+
381
379
  Args:
382
380
  devices: List of all devices
383
381
  pids: Dictionary containing device type PIDs
384
-
382
+
385
383
  Returns:
386
384
  List of sensor devices only
387
385
  """
@@ -396,18 +394,18 @@ class DayBetterClient:
396
394
  for device in devices
397
395
  if device.get("deviceMoldPid", "") in sensor_pids
398
396
  ]
399
-
397
+
400
398
  def merge_device_status(
401
399
  self,
402
400
  devices: List[Dict[str, Any]],
403
401
  statuses: List[Dict[str, Any]],
404
402
  ) -> List[Dict[str, Any]]:
405
403
  """Merge device info with status info.
406
-
404
+
407
405
  Args:
408
406
  devices: List of device info dictionaries
409
407
  statuses: List of device status dictionaries
410
-
408
+
411
409
  Returns:
412
410
  List of merged device dictionaries
413
411
  """
@@ -424,38 +422,34 @@ class DayBetterClient:
424
422
  merged.append(merged_device)
425
423
 
426
424
  return merged
427
-
425
+
428
426
  async def fetch_sensor_data(self) -> List[Dict[str, Any]]:
429
427
  """Fetch and process sensor data in one call.
430
-
428
+
431
429
  This method fetches device statuses, devices list, and PIDs,
432
430
  filters for sensor devices, and merges the data.
433
-
431
+
434
432
  Returns:
435
433
  List of sensor devices with merged status data
436
-
434
+
437
435
  Raises:
438
436
  AuthenticationError: If authentication fails
439
437
  APIError: If API request fails
440
438
  """
441
- # Fetch current statuses
442
439
  statuses = await self.fetch_device_statuses()
443
-
444
- # Fetch devices and PIDs if not cached
440
+
445
441
  if not self._devices or not self._pids:
446
442
  self._devices = await self.fetch_devices()
447
443
  self._pids = await self.fetch_pids()
448
-
449
- # Filter to sensor devices only
444
+
450
445
  sensor_devices = self.filter_sensor_devices(self._devices, self._pids)
451
-
452
- # Merge with current status
446
+
453
447
  merged = self.merge_device_status(sensor_devices, statuses)
454
448
  _LOGGER.debug("Fetched %d sensor devices", len(merged))
455
449
  return merged
456
-
457
- async def close(self):
458
- """Close the client session."""
459
- if self._session:
450
+
451
+ async def close(self) -> None:
452
+ """Close the client session if this client created it."""
453
+ if self._own_session and self._session is not None:
460
454
  await self._session.close()
461
455
  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)
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: daybetter-services-python
3
+ Version: 1.0.8
4
+ Summary: Python client for DayBetter devices and services
5
+ Author-email: THDayBetter <chenp2368@163.com>
6
+ Maintainer-email: THDayBetter <chenp2368@163.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/THDayBetter/daybetter-python
9
+ Project-URL: Documentation, https://github.com/THDayBetter/daybetter-python#readme
10
+ Project-URL: Repository, https://github.com/THDayBetter/daybetter-python.git
11
+ Project-URL: Bug Tracker, https://github.com/THDayBetter/daybetter-python/issues
12
+ Keywords: daybetter,iot,home automation,mqtt
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.8
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: aiohttp>=3.8.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=6.0; extra == "dev"
29
+ Requires-Dist: pytest-asyncio; extra == "dev"
30
+ Requires-Dist: black; extra == "dev"
31
+ Requires-Dist: isort; extra == "dev"
32
+ Requires-Dist: flake8; extra == "dev"
33
+ Requires-Dist: mypy; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # DayBetter Services Python Client
37
+
38
+ [![PyPI](https://img.shields.io/pypi/v/daybetter-services-python.svg)](https://pypi.org/project/daybetter-services-python/)
39
+ [![Python Versions](https://img.shields.io/pypi/pyversions/daybetter-services-python.svg)](https://pypi.org/project/daybetter-services-python/)
40
+ [![License](https://img.shields.io/pypi/l/daybetter-services-python.svg)](LICENSE)
41
+
42
+ Asynchronous client library used by the [Home Assistant DayBetter Services integration](https://github.com/home-assistant/core/pull/154677).
43
+ It handles authentication, device discovery, status polling, PID metadata and device control against the DayBetter cloud API.
44
+
45
+ ## Features
46
+
47
+ - Fully async `DayBetterClient` with context-manager support
48
+ - Automatic environment selection (test/prod) based on `hass_code`
49
+ - Convenience helpers for common API endpoints (devices, statuses, MQTT config)
50
+ - Sensor-focused workflow `fetch_sensor_data()` that merges devices, statuses and PID filters
51
+ - Type hints + `py.typed` for first-class editor / mypy support
52
+ - Published on PyPI for Home Assistant and other integrations
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ pip install daybetter-services-python
58
+ ```
59
+
60
+ ## Quick Start
61
+
62
+ ```python
63
+ import asyncio
64
+ from daybetter_python import DayBetterClient
65
+
66
+ async def main() -> None:
67
+ async with DayBetterClient(token="YOUR_TOKEN", hass_code="db-xxxx") as client:
68
+ # Fetch merged sensor payloads (used by Home Assistant)
69
+ sensors = await client.fetch_sensor_data()
70
+ for item in sensors:
71
+ print(item["deviceName"], item.get("temp"), item.get("humi"))
72
+
73
+ # Control a device (brightness example)
74
+ await client.control_device(
75
+ device_name="device_001",
76
+ action=True,
77
+ brightness=180,
78
+ )
79
+
80
+ if __name__ == "__main__":
81
+ asyncio.run(main())
82
+ ```
83
+
84
+ ## Home Assistant Integration
85
+
86
+ The official integration PR can be followed here: [home-assistant/core#154677](https://github.com/home-assistant/core/pull/154677).
87
+ The integration imports this library and simply calls `client.fetch_sensor_data()` inside a data coordinator, so all business logic lives in this package.
88
+
89
+ See `docs/homeassistant.md` for:
90
+ - Installation instructions (pip, custom component)
91
+ - How to obtain the DayBetter token and `hass_code`
92
+ - Sample `configuration.yaml` snippets
93
+ - Known limitations and future roadmap
94
+
95
+ ## Development
96
+
97
+ ```bash
98
+ git clone https://github.com/THDayBetter/daybetter-services-python.git
99
+ cd daybetter-services-python
100
+ python3 -m venv .venv
101
+ source .venv/bin/activate
102
+ pip install -e .[dev]
103
+
104
+ # Quality checks
105
+ flake8 daybetter_python
106
+ mypy daybetter_python
107
+ pytest
108
+ ```
109
+
110
+ Pull requests are welcome! Please open an issue if you encounter problems with the DayBetter cloud API.
111
+
112
+ ## Release Process
113
+
114
+ 1. Update `pyproject.toml` and `daybetter_python/__init__.py` with the new version.
115
+ 2. Document changes in `CHANGELOG.md`.
116
+ 3. Build and upload:
117
+ ```bash
118
+ rm -rf dist build *.egg-info
119
+ python -m build
120
+ twine check dist/*
121
+ twine upload dist/*
122
+ ```
123
+ 4. Create a Git tag (e.g., `git tag v1.0.7 && git push origin v1.0.7`).
124
+
125
+ ## License
126
+
127
+ This project is licensed under the [MIT License](LICENSE).
@@ -1,12 +1,13 @@
1
1
  LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
- setup.py
5
4
  daybetter_python/__init__.py
6
5
  daybetter_python/client.py
7
6
  daybetter_python/exceptions.py
7
+ daybetter_python/py.typed
8
8
  daybetter_services_python.egg-info/PKG-INFO
9
9
  daybetter_services_python.egg-info/SOURCES.txt
10
10
  daybetter_services_python.egg-info/dependency_links.txt
11
11
  daybetter_services_python.egg-info/requires.txt
12
- daybetter_services_python.egg-info/top_level.txt
12
+ daybetter_services_python.egg-info/top_level.txt
13
+ tests/test_placeholder.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "daybetter-services-python"
7
- version = "1.0.6"
7
+ version = "1.0.8"
8
8
  description = "Python client for DayBetter devices and services"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -25,6 +25,7 @@ classifiers = [
25
25
  "Programming Language :: Python :: 3.10",
26
26
  "Programming Language :: Python :: 3.11",
27
27
  "Programming Language :: Python :: 3.12",
28
+ "Typing :: Typed",
28
29
  ]
29
30
  requires-python = ">=3.8"
30
31
  dependencies = [
@@ -55,3 +56,10 @@ target-version = ['py38']
55
56
  [tool.isort]
56
57
  profile = "black"
57
58
  line_length = 88
59
+
60
+ [tool.flake8]
61
+ max-line-length = 100
62
+ extend-ignore = ["W503"]
63
+
64
+ [tool.setuptools.package-data]
65
+ daybetter_python = ["py.typed"]
@@ -0,0 +1,7 @@
1
+ from daybetter_python import DayBetterClient
2
+
3
+
4
+ def test_client_attributes() -> None:
5
+ client = DayBetterClient(token="abc123")
6
+ assert client.token == "abc123"
7
+
@@ -1,98 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: daybetter-services-python
3
- Version: 1.0.6
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
- Requires-Python: >=3.8
25
- Description-Content-Type: text/markdown
26
- License-File: LICENSE
27
- Requires-Dist: aiohttp>=3.8.0
28
- Provides-Extra: dev
29
- Requires-Dist: pytest>=6.0; extra == "dev"
30
- Requires-Dist: pytest-asyncio; extra == "dev"
31
- Requires-Dist: black; extra == "dev"
32
- Requires-Dist: isort; extra == "dev"
33
- Requires-Dist: flake8; extra == "dev"
34
- Requires-Dist: mypy; extra == "dev"
35
- Dynamic: author
36
- Dynamic: home-page
37
- Dynamic: license-file
38
- Dynamic: requires-python
39
-
40
- # DayBetter Python Client
41
-
42
- A Python client library for interacting with DayBetter devices and services.
43
-
44
- ## Features
45
-
46
- - Device management and control
47
- - MQTT configuration retrieval
48
- - Authentication handling
49
- - Async/await support
50
-
51
- ## Installation
52
-
53
- ```bash
54
- pip install daybetter-services-python
55
- ```
56
-
57
- ## Usage
58
-
59
- ```python
60
- import asyncio
61
- from daybetter_python import DayBetterClient
62
-
63
- async def main():
64
- async with DayBetterClient(token="your_token") as client:
65
- # Fetch devices
66
- devices = await client.fetch_devices()
67
- print(f"Found {len(devices)} devices")
68
-
69
- # Control a device
70
- result = await client.control_device(
71
- device_name="device_001",
72
- action=True,
73
- brightness=80
74
- )
75
- print(f"Control result: {result}")
76
-
77
- if __name__ == "__main__":
78
- asyncio.run(main())
79
- ```
80
-
81
- ## API Reference
82
-
83
- ### DayBetterClient
84
-
85
- #### Methods
86
-
87
- - `fetch_devices()`: Get list of devices
88
- - `fetch_pids()`: Get device type PIDs
89
- - `control_device(device_name, action, brightness, hs_color, color_temp)`: Control a device
90
- - `fetch_mqtt_config()`: Get MQTT configuration
91
-
92
- ## License
93
-
94
- MIT License
95
-
96
- ## Contributing
97
-
98
- Contributions are welcome! Please feel free to submit a Pull Request.
@@ -1,59 +0,0 @@
1
- # DayBetter Python Client
2
-
3
- A Python client library for interacting with DayBetter devices and services.
4
-
5
- ## Features
6
-
7
- - Device management and control
8
- - MQTT configuration retrieval
9
- - Authentication handling
10
- - Async/await support
11
-
12
- ## Installation
13
-
14
- ```bash
15
- pip install daybetter-services-python
16
- ```
17
-
18
- ## Usage
19
-
20
- ```python
21
- import asyncio
22
- from daybetter_python import DayBetterClient
23
-
24
- async def main():
25
- async with DayBetterClient(token="your_token") as client:
26
- # Fetch devices
27
- devices = await client.fetch_devices()
28
- print(f"Found {len(devices)} devices")
29
-
30
- # Control a device
31
- result = await client.control_device(
32
- device_name="device_001",
33
- action=True,
34
- brightness=80
35
- )
36
- print(f"Control result: {result}")
37
-
38
- if __name__ == "__main__":
39
- asyncio.run(main())
40
- ```
41
-
42
- ## API Reference
43
-
44
- ### DayBetterClient
45
-
46
- #### Methods
47
-
48
- - `fetch_devices()`: Get list of devices
49
- - `fetch_pids()`: Get device type PIDs
50
- - `control_device(device_name, action, brightness, hs_color, color_temp)`: Control a device
51
- - `fetch_mqtt_config()`: Get MQTT configuration
52
-
53
- ## License
54
-
55
- MIT License
56
-
57
- ## Contributing
58
-
59
- Contributions are welcome! Please feel free to submit a Pull Request.
@@ -1,13 +0,0 @@
1
- """DayBetter client exceptions."""
2
-
3
-
4
- class DayBetterError(Exception):
5
- """Base exception for DayBetter client."""
6
-
7
-
8
- class AuthenticationError(DayBetterError):
9
- """Authentication failed."""
10
-
11
-
12
- class APIError(DayBetterError):
13
- """API request failed."""
@@ -1,98 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: daybetter-services-python
3
- Version: 1.0.6
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
- Requires-Python: >=3.8
25
- Description-Content-Type: text/markdown
26
- License-File: LICENSE
27
- Requires-Dist: aiohttp>=3.8.0
28
- Provides-Extra: dev
29
- Requires-Dist: pytest>=6.0; extra == "dev"
30
- Requires-Dist: pytest-asyncio; extra == "dev"
31
- Requires-Dist: black; extra == "dev"
32
- Requires-Dist: isort; extra == "dev"
33
- Requires-Dist: flake8; extra == "dev"
34
- Requires-Dist: mypy; extra == "dev"
35
- Dynamic: author
36
- Dynamic: home-page
37
- Dynamic: license-file
38
- Dynamic: requires-python
39
-
40
- # DayBetter Python Client
41
-
42
- A Python client library for interacting with DayBetter devices and services.
43
-
44
- ## Features
45
-
46
- - Device management and control
47
- - MQTT configuration retrieval
48
- - Authentication handling
49
- - Async/await support
50
-
51
- ## Installation
52
-
53
- ```bash
54
- pip install daybetter-services-python
55
- ```
56
-
57
- ## Usage
58
-
59
- ```python
60
- import asyncio
61
- from daybetter_python import DayBetterClient
62
-
63
- async def main():
64
- async with DayBetterClient(token="your_token") as client:
65
- # Fetch devices
66
- devices = await client.fetch_devices()
67
- print(f"Found {len(devices)} devices")
68
-
69
- # Control a device
70
- result = await client.control_device(
71
- device_name="device_001",
72
- action=True,
73
- brightness=80
74
- )
75
- print(f"Control result: {result}")
76
-
77
- if __name__ == "__main__":
78
- asyncio.run(main())
79
- ```
80
-
81
- ## API Reference
82
-
83
- ### DayBetterClient
84
-
85
- #### Methods
86
-
87
- - `fetch_devices()`: Get list of devices
88
- - `fetch_pids()`: Get device type PIDs
89
- - `control_device(device_name, action, brightness, hs_color, color_temp)`: Control a device
90
- - `fetch_mqtt_config()`: Get MQTT configuration
91
-
92
- ## License
93
-
94
- MIT License
95
-
96
- ## Contributing
97
-
98
- Contributions are welcome! Please feel free to submit a Pull Request.
@@ -1,34 +0,0 @@
1
- from setuptools import setup, find_packages
2
-
3
- with open("README.md", "r", encoding="utf-8") as fh:
4
- long_description = fh.read()
5
-
6
- setup(
7
- name="daybetter-services-python",
8
- version="1.0.6",
9
- author="THDayBetter",
10
- author_email="chenp2368@163.com",
11
- description="Python client for DayBetter devices and services",
12
- long_description=long_description,
13
- long_description_content_type="text/markdown",
14
- url="https://github.com/THDayBetter/daybetter-python",
15
- packages=find_packages(),
16
- classifiers=[
17
- "Development Status :: 4 - Beta",
18
- "Intended Audience :: Developers",
19
- "Operating System :: OS Independent",
20
- "Programming Language :: Python :: 3",
21
- "Programming Language :: Python :: 3.8",
22
- "Programming Language :: Python :: 3.9",
23
- "Programming Language :: Python :: 3.10",
24
- "Programming Language :: Python :: 3.11",
25
- "Programming Language :: Python :: 3.12",
26
- ],
27
- python_requires=">=3.8",
28
- install_requires=["aiohttp>=3.8.0"],
29
- keywords="daybetter, iot, home automation, mqtt",
30
- project_urls={
31
- "Bug Reports": "https://github.com/THDayBetter/daybetter-python/issues",
32
- "Source": "https://github.com/THDayBetter/daybetter-python",
33
- },
34
- )