Metadata-Version: 2.4
Name: qingping-cgd1
Version: 0.1.0
Summary: Pure-Python BLE library for the Qingping CGD1 alarm clock
Project-URL: Homepage, https://github.com/rjocoleman/qingping-cgd1
Project-URL: Source, https://github.com/rjocoleman/qingping-cgd1
Author: Robert Coleman
License-Expression: MIT
License-File: LICENSE
Keywords: alarm-clock,ble,bluetooth,cgd1,qingping
Requires-Python: >=3.14.2
Requires-Dist: bleak-retry-connector>=3.5
Requires-Dist: bleak>=0.22
Description-Content-Type: text/markdown

# qingping-cgd1

Local BLE control for the Qingping CGD1 "Dove" alarm clock. Pure Python, no
cloud. Not affiliated with Qingping.

## Install

```
pip install qingping-cgd1
```

Requires Python 3.14.2+. Depends only on `bleak` and `bleak-retry-connector`.

## Passive sensor reading

Temperature, humidity, and battery ride in the BLE advertisement, so you can
read them without connecting to the device:

```python
from qingping_cgd1 import parse_advertisement

reading = parse_advertisement(service_data)  # the 0xfdcd service-data bytes
if reading is not None:
    print(reading.temperature, reading.humidity, reading.battery)
```

## Active control

Everything else - settings, alarms, time sync - needs a connection,
authenticated with a `bleak` `BLEDevice`:

```python
from qingping_cgd1 import Alarm, QingpingCGD1Client, Weekday

async with QingpingCGD1Client(ble_device) as client:
    settings = await client.read_settings()
    await client.write_settings(settings)

    alarms = await client.read_alarms()
    await client.write_alarm(0, Alarm(
        enabled=True,
        hour=7,
        minute=30,
        days=frozenset({Weekday.MONDAY, Weekday.TUESDAY}),
        snooze=True,
    ))
    await client.delete_alarm(1)

    await client.sync_time()
    info = await client.read_firmware()
```

`QingpingCGD1Client` also has explicit `connect()`/`disconnect()` methods if
you'd rather not use it as a context manager. It authenticates with a
16-byte token (overridable via the `token` argument, default
`DEFAULT_AUTH_TOKEN`), retries a timed-out command once after a reconnect,
and disconnects automatically after `disconnect_delay` seconds of
inactivity (default 120s).

### Working out the next alarm

`next_alarm` takes the list from `read_alarms()` and a reference time, and
returns the soonest upcoming fire time (or `None` if nothing is enabled):

```python
from datetime import datetime

from qingping_cgd1 import next_alarm

upcoming = next_alarm(alarms, datetime.now())
```

## Protocol notes

This library is reverse-engineered, not an official SDK. The protocol was
mapped mainly by [MrBoombastic/clOwOck](https://github.com/MrBoombastic/clOwOck),
an Android app for the CGD1; this is a Python port of that work. A few things
worth knowing before you rely on it:

- There are no checksums on the wire. Frames are fixed-layout and unvalidated
  by the device beyond their length.
- The 16-byte auth token is a per-device pairing secret, not a universal
  key. A clock that's already paired (for example, one that's been used
  with the official Qingping+ app) will reject the default token: the
  device answers auth step 2 with `04 ff 02 00 01` (fail). You need to
  unbind/reset the clock first - long-press its button until the
  Bluetooth icon flashes, or remove it in the Qingping+ app - after which
  it binds to the first token it's presented with. This library presents
  its default token, and from then on that token authenticates every
  time. The token is injectable via the `token` argument if you'd rather
  manage pairing secrets yourself.
- The `0xfdcd` advertisement layout (`model | mac | temperature | humidity |
  battery`) is confirmed from a real CGD1 capture (firmware 1.0.1_0130):
  decoding it gave 20.0 C / 51.7% / 80%, matching the device's own
  display. There are still two unknown/reserved byte pairs in the frame
  whose meaning isn't established.

## Scope

Covers passive sensor reading from advertisements, settings read/write,
all 16 alarm slots, and time sync. Ringtone upload is not implemented.

## How this was built

Built largely with AI assistance (Claude) and tested against a real CGD1
(firmware 1.0.1_0130). Tested and working, but a spare-time project - no
warranty, no support promises.

## Licence

MIT.
