daybetter-ble-client 1.0.0rc1__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.
- daybetter_ble_client-1.0.0rc1/LICENSE +21 -0
- daybetter_ble_client-1.0.0rc1/PKG-INFO +230 -0
- daybetter_ble_client-1.0.0rc1/README.md +213 -0
- daybetter_ble_client-1.0.0rc1/pyproject.toml +57 -0
- daybetter_ble_client-1.0.0rc1/setup.cfg +4 -0
- daybetter_ble_client-1.0.0rc1/src/daybetter_ble_client/__init__.py +3 -0
- daybetter_ble_client-1.0.0rc1/src/daybetter_ble_client/const.py +101 -0
- daybetter_ble_client-1.0.0rc1/src/daybetter_ble_client/led_strip.py +352 -0
- daybetter_ble_client-1.0.0rc1/src/daybetter_ble_client/util.py +310 -0
- daybetter_ble_client-1.0.0rc1/src/daybetter_ble_client.egg-info/PKG-INFO +230 -0
- daybetter_ble_client-1.0.0rc1/src/daybetter_ble_client.egg-info/SOURCES.txt +16 -0
- daybetter_ble_client-1.0.0rc1/src/daybetter_ble_client.egg-info/dependency_links.txt +1 -0
- daybetter_ble_client-1.0.0rc1/src/daybetter_ble_client.egg-info/requires.txt +2 -0
- daybetter_ble_client-1.0.0rc1/src/daybetter_ble_client.egg-info/top_level.txt +1 -0
- daybetter_ble_client-1.0.0rc1/tests/test_behavior.py +312 -0
- daybetter_ble_client-1.0.0rc1/tests/test_led_strip.py +259 -0
- daybetter_ble_client-1.0.0rc1/tests/test_offline.py +18 -0
- daybetter_ble_client-1.0.0rc1/tests/test_release.py +74 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Siddhant Kameswar
|
|
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,230 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: daybetter-ble-client
|
|
3
|
+
Version: 1.0.0rc1
|
|
4
|
+
Summary: Async Bluetooth Low Energy control for Daybetter P031 and P065 LED strips
|
|
5
|
+
Author-email: Siddhant Kameswar <61r7jw39@kameswar.com>, Jack Stockley <jack@jstockley.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jnstockley/daybetter-led-strip
|
|
8
|
+
Project-URL: Repository, https://github.com/jnstockley/daybetter-led-strip.git
|
|
9
|
+
Project-URL: Issues, https://github.com/jnstockley/daybetter-led-strip/issues
|
|
10
|
+
Project-URL: Upstream, https://github.com/grimsteel/daybetter-led-strip
|
|
11
|
+
Requires-Python: <3.16,>=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: bleak<4,>=3.1.0
|
|
15
|
+
Requires-Dist: bleak-retry-connector<5,>=4.7.1
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# daybetter-ble-client
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
Async Python control for Daybetter P031 and P065 RGB LED strips over Bluetooth
|
|
24
|
+
Low Energy, using [Bleak](https://bleak.readthedocs.io/en/latest/). Requires
|
|
25
|
+
Python 3.11–3.15. Models are detected from advertised services, with device names
|
|
26
|
+
as a fallback.
|
|
27
|
+
|
|
28
|
+
This project is forked from [grimsteel/daybetter-led-strip](https://github.com/grimsteel/daybetter-led-strip),
|
|
29
|
+
originally created by **Siddhant Kameswar** ([grimsteel](https://github.com/grimsteel)).
|
|
30
|
+
Credit for the original library and documented BLE protocol belongs to the
|
|
31
|
+
original author. The original MIT license and copyright notice are preserved
|
|
32
|
+
in [LICENSE](LICENSE).
|
|
33
|
+
|
|
34
|
+
## Installation and migration
|
|
35
|
+
|
|
36
|
+
The new distribution name is **`daybetter-ble-client`**, and the import package
|
|
37
|
+
is **`daybetter_ble_client`**. This intentionally changes the old
|
|
38
|
+
`daybetter_led_strip` import path; class and method names remain the same:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from daybetter_ble_client import DaybetterLedStrip
|
|
42
|
+
from daybetter_ble_client.const import SUPPORTED_DISCOVERY_SERVICES
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Until the first release of this fork is published, install this checkout with
|
|
46
|
+
`uv sync --locked`. Once published, install the distribution with
|
|
47
|
+
`uv pip install daybetter-ble-client` (or `pip install daybetter-ble-client`).
|
|
48
|
+
The upstream `daybetter-led-strip` distribution does not include this fork's
|
|
49
|
+
changes.
|
|
50
|
+
|
|
51
|
+
## Local development
|
|
52
|
+
|
|
53
|
+
Install [uv](https://docs.astral.sh/uv/) and run:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
uv sync --locked --all-groups
|
|
57
|
+
uv run --locked pytest
|
|
58
|
+
uv run --locked pytest tests/examples
|
|
59
|
+
uv run --locked ruff check
|
|
60
|
+
uv run --locked ruff format --check
|
|
61
|
+
uv build --no-sources
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`.python-version` selects Python 3.15 locally. Commit `uv.lock` when changing
|
|
65
|
+
dependencies; `pyproject.toml` is the only dependency manifest.
|
|
66
|
+
Runtime dependencies have explicit major-version bounds: Bleak stays on 3.x
|
|
67
|
+
and bleak-retry-connector stays on 4.x. A major upgrade requires changing those
|
|
68
|
+
bounds deliberately; ordinary dependency updates stay within them.
|
|
69
|
+
|
|
70
|
+
`./scripts/test.sh` runs the library/release-tool tests with branch coverage and
|
|
71
|
+
then the example tests. Coverage must be at least 95%; reports are written to
|
|
72
|
+
`logs/`. Both CI tests and linting run on Python 3.11, 3.12, 3.13, 3.14,
|
|
73
|
+
and 3.15. `./scripts/lint.sh` additionally checks shell and YAML files, requiring
|
|
74
|
+
`shellcheck`, `shfmt`, and `yamllint` on PATH. See [testing](docs/TESTING.md) and
|
|
75
|
+
[releases](docs/RELEASING.md) for the full workflow.
|
|
76
|
+
|
|
77
|
+
The repository uses `src/daybetter_ble_client/` for the library, `tests/` for
|
|
78
|
+
offline tests, `tests/examples/` for example tests, `examples/` for runnable
|
|
79
|
+
programs, and `scripts/` for validation/release helpers. Tests block real BLE
|
|
80
|
+
access; they do not need hardware or a PyPI account.
|
|
81
|
+
|
|
82
|
+
For actual device use, enable Bluetooth and grant Bluetooth access to the app
|
|
83
|
+
running Python on macOS. Close the phone app before connecting; a strip accepts
|
|
84
|
+
only one BLE connection at a time.
|
|
85
|
+
|
|
86
|
+
## Usage
|
|
87
|
+
|
|
88
|
+
The `DaybetterLedStrip` constructor only requires the device address (a UUID on
|
|
89
|
+
macOS, a MAC address on other platforms). Pass the scanned device and its
|
|
90
|
+
advertisement data to `.update_device` for automatic model selection. The
|
|
91
|
+
detected model is available as `.model`. If identifying data is unavailable,
|
|
92
|
+
the compatibility default is P031. If the device disconnects, it will not
|
|
93
|
+
automatically reconnect; call `.update_device` again with a freshly scanned
|
|
94
|
+
device, or `.connect` to retry the stored device. P065 authenticates on every
|
|
95
|
+
connection, and initialization failures raise an exception after cleanup.
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import asyncio
|
|
99
|
+
from bleak import BleakScanner
|
|
100
|
+
from daybetter_ble_client import DaybetterLedStrip
|
|
101
|
+
from daybetter_ble_client.const import SUPPORTED_DISCOVERY_SERVICES
|
|
102
|
+
|
|
103
|
+
# This snippet runs inside an async function. Pick one target explicitly.
|
|
104
|
+
devices = await BleakScanner.discover(timeout=10, return_adv=True)
|
|
105
|
+
matches = [
|
|
106
|
+
(device, data)
|
|
107
|
+
for device, data in devices.values()
|
|
108
|
+
if any(service in data.service_uuids for service in SUPPORTED_DISCOVERY_SERVICES)
|
|
109
|
+
]
|
|
110
|
+
for device, data in matches:
|
|
111
|
+
print(device.name, device.address)
|
|
112
|
+
if not matches:
|
|
113
|
+
raise RuntimeError("No supported strip found")
|
|
114
|
+
device, data = matches[0] # choose the intended strip if several are found
|
|
115
|
+
led_strip = DaybetterLedStrip(device.address)
|
|
116
|
+
try:
|
|
117
|
+
await led_strip.update_device(device, data)
|
|
118
|
+
if not led_strip.connected:
|
|
119
|
+
raise RuntimeError("Could not connect to strip")
|
|
120
|
+
await led_strip.set_power(True)
|
|
121
|
+
await led_strip.set_color((255, 0, 0))
|
|
122
|
+
await asyncio.sleep(1)
|
|
123
|
+
await led_strip.set_power(False)
|
|
124
|
+
finally:
|
|
125
|
+
await led_strip.disconnect()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A complete example program is provided in `examples/test.py`.
|
|
129
|
+
Run it with `uv run --locked python examples/test.py`. It cycles colors, changes brightness,
|
|
130
|
+
and turns the strip off. It requires exactly one supported strip to be
|
|
131
|
+
discoverable to avoid changing an unintended device.
|
|
132
|
+
|
|
133
|
+
This package was created [for use in Home Assistant](https://github.com/grimsteel/homeassistant-daybetter-led-strip), which may explain some of the API choices.
|
|
134
|
+
|
|
135
|
+
## Protocol
|
|
136
|
+
|
|
137
|
+
Communication with the LED strip is done over BLE.
|
|
138
|
+
|
|
139
|
+
The light strip exposes two services: the generic access profile w/ the device name, and a custom "LED control service" (`0xe031`).
|
|
140
|
+
|
|
141
|
+
During discovery, the device also advertises itself with a third service (`0xc031`) for identification.
|
|
142
|
+
|
|
143
|
+
P065 uses the equivalent UUIDs `0xc065` (discovery), `0xe065` (control
|
|
144
|
+
service), `0xa065` (write), and `0xf065` (status notifications).
|
|
145
|
+
|
|
146
|
+
### P065 connection initialization
|
|
147
|
+
|
|
148
|
+
After subscribing to notifications, P065 requires a two-step handshake before
|
|
149
|
+
control commands take effect. Send command `0x00` with `CCHIP`; the response
|
|
150
|
+
contains `CCHIP` and two device-specific bytes. Echo that complete seven-byte
|
|
151
|
+
payload with command `0x01`. A six-byte reply ending in `0x01` indicates success.
|
|
152
|
+
The library validates framing and CRC and waits up to five seconds for each
|
|
153
|
+
response. It disconnects if initialization fails. Do not hard-code the two
|
|
154
|
+
device-specific bytes from another strip.
|
|
155
|
+
|
|
156
|
+
P065 color commands append `0xff` after the RGB bytes, and effect commands
|
|
157
|
+
append `0xff` after the effect ID. P031 payloads retain their existing format.
|
|
158
|
+
An acknowledgement only confirms receipt; it does not prove a visible change.
|
|
159
|
+
|
|
160
|
+
Validation on a physical P065 covered handshake/reconnect, RGB color, brightness,
|
|
161
|
+
power, and `FADE_RED_GREEN`, with settings checked through status reads. Other
|
|
162
|
+
effect IDs have not been individually tested. P031's original command format is
|
|
163
|
+
covered by regression tests; a fresh P031 hardware check could not be completed
|
|
164
|
+
during this change because the device was not consistently discoverable.
|
|
165
|
+
|
|
166
|
+
The LED control service has a read characteristic (`0xf031`) and a write characteristic (`0xa031`). As far as I can tell, the read characteristic cannot actually be read directly. Rather, if notifications are enabled on this service, the device will push updates about its state at various times.
|
|
167
|
+
|
|
168
|
+
### Write Data Format
|
|
169
|
+
|
|
170
|
+
Commands written to the write characteristic have the following format.
|
|
171
|
+
|
|
172
|
+
1. The byte `0xA0` (referred to in the app source code as "APP_TO_DEVICE")
|
|
173
|
+
2. The command ID. (1 byte)
|
|
174
|
+
3. The length of the entire message, not including the checksum. (1 byte) `= len(payload) + 3`
|
|
175
|
+
4. The payload for this command
|
|
176
|
+
5. The little-endian CRC16 MODBUS checksum of the rest of the message.
|
|
177
|
+
|
|
178
|
+
### Commands
|
|
179
|
+
|
|
180
|
+
| Command | ID | Payload Description |
|
|
181
|
+
|-----------------|--------|-----------------------------------------------------------------------------------------------------|
|
|
182
|
+
| **Get status** | `0x10` | `0x00`<br>Results in status payload below being sent on notify |
|
|
183
|
+
| **Set power** | `0x11` | `0x01` (on) or `0x00` (off) |
|
|
184
|
+
| **Show effect** | `0x12` | One of the effect bytes listed below |
|
|
185
|
+
| **Set brightness** | `0x13` | A byte representing the brightness, from 0 (`0x00`) to 100 (`0x64`). Brightness 0 still has the LEDs slightly on |
|
|
186
|
+
| **Set color** | `0x15` | 3 bytes, in RGB order |
|
|
187
|
+
|
|
188
|
+
Example command (set to red): `A0 15 06 FF 00 00 25 C0`
|
|
189
|
+
|
|
190
|
+
### Notify/read data format
|
|
191
|
+
|
|
192
|
+
The payloads received over notify have a similar structure as the write commands:
|
|
193
|
+
|
|
194
|
+
1. The byte `0xA1` ("DEVICE_TO_APP")
|
|
195
|
+
2. Command ID
|
|
196
|
+
3. Length of entire message excluding checksum
|
|
197
|
+
4. Payload
|
|
198
|
+
5. LE Checksum
|
|
199
|
+
|
|
200
|
+
### Received "commands"
|
|
201
|
+
|
|
202
|
+
When any of the above write commands are written to the characteristic successfully, the device responds with a message with the same command ID and `0x01` for the payload.
|
|
203
|
+
|
|
204
|
+
For instance, the above color command would result in `A1 15 04 01 F1 1C`
|
|
205
|
+
|
|
206
|
+
When the strip is turned on/off from the IR remote, a message with command ID `0x10` and the payload `00 01 01 64 32 32 ff 31 2d f0 00 00 00 00` (off) or `01 01 01 64 32 32 ff 31 2d f0 00 00 00 00`. The first byte indicates the on/off status, but I don't know what the rest means. It seems unrelated to the color of the light.
|
|
207
|
+
|
|
208
|
+
Running the "get status" command also results in this being sent.
|
|
209
|
+
|
|
210
|
+
### Effects
|
|
211
|
+
|
|
212
|
+
> Note: the brightness can be changed during the following effects. Lower brightnesses result in blinking happening faster, likely because there is a smaller range of brightnesses to interpolate between.
|
|
213
|
+
|
|
214
|
+
RGB: just red/green/blue
|
|
215
|
+
|
|
216
|
+
All preset colors: red/green/blue/yellow/teal/purple/white
|
|
217
|
+
|
|
218
|
+
* `0x02..0x08`: show preset colors
|
|
219
|
+
* `0x09`: switch RGB abruptly, no transition
|
|
220
|
+
* `0x0A`: switch all preset colors
|
|
221
|
+
* `0x0B`: fade between colors quickly
|
|
222
|
+
* `0x0C`: fade between colors slowly
|
|
223
|
+
* `0x0D`..`0x13`: blink individual preset colors
|
|
224
|
+
* `0x14`: fade red/green
|
|
225
|
+
* `0x15`: fade red/blue
|
|
226
|
+
* `0x16`: fade green/blue
|
|
227
|
+
* `0x17`: flash all preset colors
|
|
228
|
+
* `0x18`..`0x1E`: flash individual preset colors
|
|
229
|
+
* `0x1F`: strobe RGB
|
|
230
|
+
* `0x20`: strobe all colors
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# daybetter-ble-client
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
Async Python control for Daybetter P031 and P065 RGB LED strips over Bluetooth
|
|
7
|
+
Low Energy, using [Bleak](https://bleak.readthedocs.io/en/latest/). Requires
|
|
8
|
+
Python 3.11–3.15. Models are detected from advertised services, with device names
|
|
9
|
+
as a fallback.
|
|
10
|
+
|
|
11
|
+
This project is forked from [grimsteel/daybetter-led-strip](https://github.com/grimsteel/daybetter-led-strip),
|
|
12
|
+
originally created by **Siddhant Kameswar** ([grimsteel](https://github.com/grimsteel)).
|
|
13
|
+
Credit for the original library and documented BLE protocol belongs to the
|
|
14
|
+
original author. The original MIT license and copyright notice are preserved
|
|
15
|
+
in [LICENSE](LICENSE).
|
|
16
|
+
|
|
17
|
+
## Installation and migration
|
|
18
|
+
|
|
19
|
+
The new distribution name is **`daybetter-ble-client`**, and the import package
|
|
20
|
+
is **`daybetter_ble_client`**. This intentionally changes the old
|
|
21
|
+
`daybetter_led_strip` import path; class and method names remain the same:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from daybetter_ble_client import DaybetterLedStrip
|
|
25
|
+
from daybetter_ble_client.const import SUPPORTED_DISCOVERY_SERVICES
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Until the first release of this fork is published, install this checkout with
|
|
29
|
+
`uv sync --locked`. Once published, install the distribution with
|
|
30
|
+
`uv pip install daybetter-ble-client` (or `pip install daybetter-ble-client`).
|
|
31
|
+
The upstream `daybetter-led-strip` distribution does not include this fork's
|
|
32
|
+
changes.
|
|
33
|
+
|
|
34
|
+
## Local development
|
|
35
|
+
|
|
36
|
+
Install [uv](https://docs.astral.sh/uv/) and run:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
uv sync --locked --all-groups
|
|
40
|
+
uv run --locked pytest
|
|
41
|
+
uv run --locked pytest tests/examples
|
|
42
|
+
uv run --locked ruff check
|
|
43
|
+
uv run --locked ruff format --check
|
|
44
|
+
uv build --no-sources
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`.python-version` selects Python 3.15 locally. Commit `uv.lock` when changing
|
|
48
|
+
dependencies; `pyproject.toml` is the only dependency manifest.
|
|
49
|
+
Runtime dependencies have explicit major-version bounds: Bleak stays on 3.x
|
|
50
|
+
and bleak-retry-connector stays on 4.x. A major upgrade requires changing those
|
|
51
|
+
bounds deliberately; ordinary dependency updates stay within them.
|
|
52
|
+
|
|
53
|
+
`./scripts/test.sh` runs the library/release-tool tests with branch coverage and
|
|
54
|
+
then the example tests. Coverage must be at least 95%; reports are written to
|
|
55
|
+
`logs/`. Both CI tests and linting run on Python 3.11, 3.12, 3.13, 3.14,
|
|
56
|
+
and 3.15. `./scripts/lint.sh` additionally checks shell and YAML files, requiring
|
|
57
|
+
`shellcheck`, `shfmt`, and `yamllint` on PATH. See [testing](docs/TESTING.md) and
|
|
58
|
+
[releases](docs/RELEASING.md) for the full workflow.
|
|
59
|
+
|
|
60
|
+
The repository uses `src/daybetter_ble_client/` for the library, `tests/` for
|
|
61
|
+
offline tests, `tests/examples/` for example tests, `examples/` for runnable
|
|
62
|
+
programs, and `scripts/` for validation/release helpers. Tests block real BLE
|
|
63
|
+
access; they do not need hardware or a PyPI account.
|
|
64
|
+
|
|
65
|
+
For actual device use, enable Bluetooth and grant Bluetooth access to the app
|
|
66
|
+
running Python on macOS. Close the phone app before connecting; a strip accepts
|
|
67
|
+
only one BLE connection at a time.
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
The `DaybetterLedStrip` constructor only requires the device address (a UUID on
|
|
72
|
+
macOS, a MAC address on other platforms). Pass the scanned device and its
|
|
73
|
+
advertisement data to `.update_device` for automatic model selection. The
|
|
74
|
+
detected model is available as `.model`. If identifying data is unavailable,
|
|
75
|
+
the compatibility default is P031. If the device disconnects, it will not
|
|
76
|
+
automatically reconnect; call `.update_device` again with a freshly scanned
|
|
77
|
+
device, or `.connect` to retry the stored device. P065 authenticates on every
|
|
78
|
+
connection, and initialization failures raise an exception after cleanup.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import asyncio
|
|
82
|
+
from bleak import BleakScanner
|
|
83
|
+
from daybetter_ble_client import DaybetterLedStrip
|
|
84
|
+
from daybetter_ble_client.const import SUPPORTED_DISCOVERY_SERVICES
|
|
85
|
+
|
|
86
|
+
# This snippet runs inside an async function. Pick one target explicitly.
|
|
87
|
+
devices = await BleakScanner.discover(timeout=10, return_adv=True)
|
|
88
|
+
matches = [
|
|
89
|
+
(device, data)
|
|
90
|
+
for device, data in devices.values()
|
|
91
|
+
if any(service in data.service_uuids for service in SUPPORTED_DISCOVERY_SERVICES)
|
|
92
|
+
]
|
|
93
|
+
for device, data in matches:
|
|
94
|
+
print(device.name, device.address)
|
|
95
|
+
if not matches:
|
|
96
|
+
raise RuntimeError("No supported strip found")
|
|
97
|
+
device, data = matches[0] # choose the intended strip if several are found
|
|
98
|
+
led_strip = DaybetterLedStrip(device.address)
|
|
99
|
+
try:
|
|
100
|
+
await led_strip.update_device(device, data)
|
|
101
|
+
if not led_strip.connected:
|
|
102
|
+
raise RuntimeError("Could not connect to strip")
|
|
103
|
+
await led_strip.set_power(True)
|
|
104
|
+
await led_strip.set_color((255, 0, 0))
|
|
105
|
+
await asyncio.sleep(1)
|
|
106
|
+
await led_strip.set_power(False)
|
|
107
|
+
finally:
|
|
108
|
+
await led_strip.disconnect()
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
A complete example program is provided in `examples/test.py`.
|
|
112
|
+
Run it with `uv run --locked python examples/test.py`. It cycles colors, changes brightness,
|
|
113
|
+
and turns the strip off. It requires exactly one supported strip to be
|
|
114
|
+
discoverable to avoid changing an unintended device.
|
|
115
|
+
|
|
116
|
+
This package was created [for use in Home Assistant](https://github.com/grimsteel/homeassistant-daybetter-led-strip), which may explain some of the API choices.
|
|
117
|
+
|
|
118
|
+
## Protocol
|
|
119
|
+
|
|
120
|
+
Communication with the LED strip is done over BLE.
|
|
121
|
+
|
|
122
|
+
The light strip exposes two services: the generic access profile w/ the device name, and a custom "LED control service" (`0xe031`).
|
|
123
|
+
|
|
124
|
+
During discovery, the device also advertises itself with a third service (`0xc031`) for identification.
|
|
125
|
+
|
|
126
|
+
P065 uses the equivalent UUIDs `0xc065` (discovery), `0xe065` (control
|
|
127
|
+
service), `0xa065` (write), and `0xf065` (status notifications).
|
|
128
|
+
|
|
129
|
+
### P065 connection initialization
|
|
130
|
+
|
|
131
|
+
After subscribing to notifications, P065 requires a two-step handshake before
|
|
132
|
+
control commands take effect. Send command `0x00` with `CCHIP`; the response
|
|
133
|
+
contains `CCHIP` and two device-specific bytes. Echo that complete seven-byte
|
|
134
|
+
payload with command `0x01`. A six-byte reply ending in `0x01` indicates success.
|
|
135
|
+
The library validates framing and CRC and waits up to five seconds for each
|
|
136
|
+
response. It disconnects if initialization fails. Do not hard-code the two
|
|
137
|
+
device-specific bytes from another strip.
|
|
138
|
+
|
|
139
|
+
P065 color commands append `0xff` after the RGB bytes, and effect commands
|
|
140
|
+
append `0xff` after the effect ID. P031 payloads retain their existing format.
|
|
141
|
+
An acknowledgement only confirms receipt; it does not prove a visible change.
|
|
142
|
+
|
|
143
|
+
Validation on a physical P065 covered handshake/reconnect, RGB color, brightness,
|
|
144
|
+
power, and `FADE_RED_GREEN`, with settings checked through status reads. Other
|
|
145
|
+
effect IDs have not been individually tested. P031's original command format is
|
|
146
|
+
covered by regression tests; a fresh P031 hardware check could not be completed
|
|
147
|
+
during this change because the device was not consistently discoverable.
|
|
148
|
+
|
|
149
|
+
The LED control service has a read characteristic (`0xf031`) and a write characteristic (`0xa031`). As far as I can tell, the read characteristic cannot actually be read directly. Rather, if notifications are enabled on this service, the device will push updates about its state at various times.
|
|
150
|
+
|
|
151
|
+
### Write Data Format
|
|
152
|
+
|
|
153
|
+
Commands written to the write characteristic have the following format.
|
|
154
|
+
|
|
155
|
+
1. The byte `0xA0` (referred to in the app source code as "APP_TO_DEVICE")
|
|
156
|
+
2. The command ID. (1 byte)
|
|
157
|
+
3. The length of the entire message, not including the checksum. (1 byte) `= len(payload) + 3`
|
|
158
|
+
4. The payload for this command
|
|
159
|
+
5. The little-endian CRC16 MODBUS checksum of the rest of the message.
|
|
160
|
+
|
|
161
|
+
### Commands
|
|
162
|
+
|
|
163
|
+
| Command | ID | Payload Description |
|
|
164
|
+
|-----------------|--------|-----------------------------------------------------------------------------------------------------|
|
|
165
|
+
| **Get status** | `0x10` | `0x00`<br>Results in status payload below being sent on notify |
|
|
166
|
+
| **Set power** | `0x11` | `0x01` (on) or `0x00` (off) |
|
|
167
|
+
| **Show effect** | `0x12` | One of the effect bytes listed below |
|
|
168
|
+
| **Set brightness** | `0x13` | A byte representing the brightness, from 0 (`0x00`) to 100 (`0x64`). Brightness 0 still has the LEDs slightly on |
|
|
169
|
+
| **Set color** | `0x15` | 3 bytes, in RGB order |
|
|
170
|
+
|
|
171
|
+
Example command (set to red): `A0 15 06 FF 00 00 25 C0`
|
|
172
|
+
|
|
173
|
+
### Notify/read data format
|
|
174
|
+
|
|
175
|
+
The payloads received over notify have a similar structure as the write commands:
|
|
176
|
+
|
|
177
|
+
1. The byte `0xA1` ("DEVICE_TO_APP")
|
|
178
|
+
2. Command ID
|
|
179
|
+
3. Length of entire message excluding checksum
|
|
180
|
+
4. Payload
|
|
181
|
+
5. LE Checksum
|
|
182
|
+
|
|
183
|
+
### Received "commands"
|
|
184
|
+
|
|
185
|
+
When any of the above write commands are written to the characteristic successfully, the device responds with a message with the same command ID and `0x01` for the payload.
|
|
186
|
+
|
|
187
|
+
For instance, the above color command would result in `A1 15 04 01 F1 1C`
|
|
188
|
+
|
|
189
|
+
When the strip is turned on/off from the IR remote, a message with command ID `0x10` and the payload `00 01 01 64 32 32 ff 31 2d f0 00 00 00 00` (off) or `01 01 01 64 32 32 ff 31 2d f0 00 00 00 00`. The first byte indicates the on/off status, but I don't know what the rest means. It seems unrelated to the color of the light.
|
|
190
|
+
|
|
191
|
+
Running the "get status" command also results in this being sent.
|
|
192
|
+
|
|
193
|
+
### Effects
|
|
194
|
+
|
|
195
|
+
> Note: the brightness can be changed during the following effects. Lower brightnesses result in blinking happening faster, likely because there is a smaller range of brightnesses to interpolate between.
|
|
196
|
+
|
|
197
|
+
RGB: just red/green/blue
|
|
198
|
+
|
|
199
|
+
All preset colors: red/green/blue/yellow/teal/purple/white
|
|
200
|
+
|
|
201
|
+
* `0x02..0x08`: show preset colors
|
|
202
|
+
* `0x09`: switch RGB abruptly, no transition
|
|
203
|
+
* `0x0A`: switch all preset colors
|
|
204
|
+
* `0x0B`: fade between colors quickly
|
|
205
|
+
* `0x0C`: fade between colors slowly
|
|
206
|
+
* `0x0D`..`0x13`: blink individual preset colors
|
|
207
|
+
* `0x14`: fade red/green
|
|
208
|
+
* `0x15`: fade red/blue
|
|
209
|
+
* `0x16`: fade green/blue
|
|
210
|
+
* `0x17`: flash all preset colors
|
|
211
|
+
* `0x18`..`0x1E`: flash individual preset colors
|
|
212
|
+
* `0x1F`: strobe RGB
|
|
213
|
+
* `0x20`: strobe all colors
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "daybetter-ble-client"
|
|
3
|
+
version = "1.0.0rc1"
|
|
4
|
+
description = "Async Bluetooth Low Energy control for Daybetter P031 and P065 LED strips"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11,<3.16"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Siddhant Kameswar", email = "61r7jw39@kameswar.com" },
|
|
11
|
+
{ name = "Jack Stockley", email = "jack@jstockley.com" },
|
|
12
|
+
]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"bleak>=3.1.0,<4",
|
|
15
|
+
"bleak-retry-connector>=4.7.1,<5",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://github.com/jnstockley/daybetter-led-strip"
|
|
20
|
+
Repository = "https://github.com/jnstockley/daybetter-led-strip.git"
|
|
21
|
+
Issues = "https://github.com/jnstockley/daybetter-led-strip/issues"
|
|
22
|
+
Upstream = "https://github.com/grimsteel/daybetter-led-strip"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["setuptools>=77"]
|
|
26
|
+
build-backend = "setuptools.build_meta"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["src"]
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
testpaths = ["tests"]
|
|
33
|
+
norecursedirs = ["examples", ".*"]
|
|
34
|
+
addopts = "-ra"
|
|
35
|
+
|
|
36
|
+
[tool.coverage.run]
|
|
37
|
+
branch = true
|
|
38
|
+
source = ["daybetter_ble_client"]
|
|
39
|
+
|
|
40
|
+
[tool.coverage.report]
|
|
41
|
+
show_missing = true
|
|
42
|
+
fail_under = 95
|
|
43
|
+
|
|
44
|
+
[dependency-groups]
|
|
45
|
+
dev = [
|
|
46
|
+
"packaging>=25.0",
|
|
47
|
+
"pytest>=9.1.1",
|
|
48
|
+
"pytest-cov>=7.1.0",
|
|
49
|
+
"ruff>=0.17.2",
|
|
50
|
+
"twine>=6.0",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
target-version = "py311"
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
select = ["E4", "E7", "E9", "F", "I"]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import IntEnum, unique
|
|
3
|
+
|
|
4
|
+
from bleak.uuids import normalize_uuid_16
|
|
5
|
+
|
|
6
|
+
# includes Device Name characteristic
|
|
7
|
+
SERVICE_GENERIC_ACCESS_PROFILE = normalize_uuid_16(0x1800)
|
|
8
|
+
# read
|
|
9
|
+
CHAR_DEVICE_NAME = normalize_uuid_16(0x2A00)
|
|
10
|
+
|
|
11
|
+
SERVICE_LED_CONTROL = normalize_uuid_16(0xE031)
|
|
12
|
+
# write
|
|
13
|
+
CHAR_LED_CONTROL = normalize_uuid_16(0xA031)
|
|
14
|
+
# "read", notify
|
|
15
|
+
CHAR_LED_STATUS = normalize_uuid_16(0xF031)
|
|
16
|
+
|
|
17
|
+
# Advertised service
|
|
18
|
+
SERVICE_DISCOVERY = normalize_uuid_16(0xC031)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class DeviceProfile:
|
|
23
|
+
model: str
|
|
24
|
+
discovery_service: str
|
|
25
|
+
control_service: str
|
|
26
|
+
write_characteristic: str
|
|
27
|
+
status_characteristic: str
|
|
28
|
+
requires_handshake: bool = False
|
|
29
|
+
variant: bytes = b""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
P031 = DeviceProfile(
|
|
33
|
+
"P031", SERVICE_DISCOVERY, SERVICE_LED_CONTROL, CHAR_LED_CONTROL, CHAR_LED_STATUS
|
|
34
|
+
)
|
|
35
|
+
P065 = DeviceProfile(
|
|
36
|
+
"P065",
|
|
37
|
+
normalize_uuid_16(0xC065),
|
|
38
|
+
normalize_uuid_16(0xE065),
|
|
39
|
+
normalize_uuid_16(0xA065),
|
|
40
|
+
normalize_uuid_16(0xF065),
|
|
41
|
+
True,
|
|
42
|
+
b"\xff",
|
|
43
|
+
)
|
|
44
|
+
DEVICE_PROFILES = (P031, P065)
|
|
45
|
+
SUPPORTED_DISCOVERY_SERVICES = tuple(p.discovery_service for p in DEVICE_PROFILES)
|
|
46
|
+
|
|
47
|
+
# GATT command ID
|
|
48
|
+
COMMAND_BRIGHTNESS = 0x13
|
|
49
|
+
COMMAND_EFFECT = 0x12
|
|
50
|
+
COMMAND_POWER = 0x11
|
|
51
|
+
COMMAND_COLOR = 0x15
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@unique
|
|
55
|
+
class Effect(IntEnum):
|
|
56
|
+
# Show preset colors
|
|
57
|
+
SHOW_RED = 0x02
|
|
58
|
+
SHOW_GREEN = 0x03
|
|
59
|
+
SHOW_BLUE = 0x04
|
|
60
|
+
SHOW_YELLOW = 0x05
|
|
61
|
+
SHOW_TEAL = 0x06
|
|
62
|
+
SHOW_PURPLE = 0x07
|
|
63
|
+
SHOW_WHITE = 0x08
|
|
64
|
+
|
|
65
|
+
# Switch colors abruptly
|
|
66
|
+
SWITCH_RGB = 0x09
|
|
67
|
+
SWITCH_ALL_PRESET_COLORS = 0x0A
|
|
68
|
+
|
|
69
|
+
# Fade between colors
|
|
70
|
+
FADE_COLORS_QUICK = 0x0B
|
|
71
|
+
FADE_COLORS_SLOW = 0x0C
|
|
72
|
+
|
|
73
|
+
# Blink individual preset colors
|
|
74
|
+
BLINK_RED = 0x0D
|
|
75
|
+
BLINK_GREEN = 0x0E
|
|
76
|
+
BLINK_BLUE = 0x0F
|
|
77
|
+
BLINK_YELLOW = 0x10
|
|
78
|
+
BLINK_TEAL = 0x11
|
|
79
|
+
BLINK_PURPLE = 0x12
|
|
80
|
+
BLINK_WHITE = 0x13
|
|
81
|
+
|
|
82
|
+
# Fade between two colors
|
|
83
|
+
FADE_RED_GREEN = 0x14
|
|
84
|
+
FADE_RED_BLUE = 0x15
|
|
85
|
+
FADE_GREEN_BLUE = 0x16
|
|
86
|
+
|
|
87
|
+
# Flash all preset colors
|
|
88
|
+
FLASH_ALL_PRESET_COLORS = 0x17
|
|
89
|
+
|
|
90
|
+
# Flash individual preset colors
|
|
91
|
+
FLASH_RED = 0x18
|
|
92
|
+
FLASH_GREEN = 0x19
|
|
93
|
+
FLASH_BLUE = 0x1A
|
|
94
|
+
FLASH_YELLOW = 0x1B
|
|
95
|
+
FLASH_TEAL = 0x1C
|
|
96
|
+
FLASH_PURPLE = 0x1D
|
|
97
|
+
FLASH_WHITE = 0x1E
|
|
98
|
+
|
|
99
|
+
# Strobe effects
|
|
100
|
+
STROBE_RGB = 0x1F
|
|
101
|
+
STROBE_ALL_COLORS = 0x20
|