Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 0 additions & 136 deletions README.md

This file was deleted.

91 changes: 91 additions & 0 deletions docs/BX2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Holman BX2 notes

Protocol notes for a Holman **BX2** (dual outlet, advertised name `BX2`) with this SDK. No device addresses or credentials belong in this tree.

`AE 8E` is a shared session unlock also used by public BX1 ESPHome configs, not a per-device secret.

## Identity

- Advertised alias is exactly `BX2` (bluetoothctl Name/Alias and Bleak `local_name`). Not `Holman BX2` or `BTX2`. Discovery matches exact names `Tap Timer` (BX1) and `BX2`.
- This BX2 advertises vendor service `c521f000-0d70-4d4f-8e43-40d84c50ab38` (already labelled BTX1 / CO3011). Discovery and connect-time service pick use the same list: CO3015, CO3012, and CO3011 by default (the on-air BX2 UUID is CO3011). Set `HOLMAN_SERVICE_UUIDS` to a comma-separated list to replace that filter, or pass `service_uuids` to `TapTimerManager`. Another BTX2 UUID (`aacaebbb-...`) stays in the default list for other units.
- Manufacturer company id `0x0374`. BLE address type is **random**.

## GATT (safe)

| UUID | Role |
| --- | --- |
| `c521f000-...` | Vendor service |
| `0000c001-...` | Write. Session unlock `AE 8E`. |
| `0000f006-...` | Write. Manual start/stop. |
| `0000f004-...` | Read. 12-byte state. Last byte `01` means running **when the official app started the valve**. |

`start()` / `stop()` send the unlock when `c001` is present.

## Manual payload

4 bytes on `f006`:

[0x01, tap_index, 0x00, minutes]

| Tap index | Zone | Start write | Hex |
| --- | --- | --- | --- |
| 0 | Zone 1 | `[0x01, 0x00, 0x00, minutes]` | `010000NN` |
| 1 | Zone 2 | `[0x01, 0x01, 0x00, minutes]` | `010100NN` |
| — | Stop | `[0x00, 0x00, 0x00, 0x00]` | `00000000` |

- Byte 0 is on/off (`0x01` start, `0x00` stop).
- Byte 1 is the tap index: `0x00` = zone 1, `0x01` = zone 2.
- `minutes` is `1...255`.
- Stop is all-off (both outlets).
- Zone 1 matches the original single-outlet SDK ON payload `01 00 00 <mins>`. BX1 stays compatible if callers leave the default tap 0.

A 10-byte ESPHome-style pad (`01 00 00 mins` + six zeros) is accepted if written **without** response. A 10-byte write **with** response returned ATT `0x0e` and dropped the link. Prefer the 4-byte form.

If a 4-byte write **with** response fails (ATT `0x0e`), retry **without** response. That is common while a run is already active.

## Dual outlet behaviour

- The timer can run **one outlet at a time**. Starting zone 2 while zone 1 is open is not a second concurrent valve.
- To switch outlets: write stop (`00 00 00 00`), then start the other tap. A start-while-running write with response often errors; stop first.
- There is no extra reset characteristic required after a manual run. Stop is the all-zero `f006` write.

## What not to do

- **Do not read `0000e002-...`.** That drops the connection.
- Reading `f004` without a prior `c001` unlock can also return ATT `0x0e` and drop the link.
- First LE connect often fails with `le-connection-abort-by-local` / "failed to discover services, device disconnected". Retry. Two clients racing the same adapter makes this worse.
- `f004` last byte is **not a reliable water-is-flowing flag** after an SDK write. The official app sets it to `01`. A 4-byte start can open the valve while last byte stays `00`. Treat a successful write as optimistic; confirm physically if it matters.
- A successful GATT write can still look dry if that outlet is blocked. Confirm water, not only BLE ACKs.

## Other characteristics

Seen on the same service, not required for manual run:

- `f003` — identity blob (MAC bytes reversed), readable without unlock.
- `f005`, `e001`, `c002` — read/write. Unlock may be required. Not needed for start/stop.
- `46a60001-ca26-425a-9bc6-d917829d2906` — write + notify. Untouched.

## App pairing vs session unlock

BlueZ `Paired`/`Bonded` can stay **no**. The Holman app still talks to the timer. Multiple phones can start a **manual** run at the same time. `AE 8E` is a session unlock, not exclusive SMP pairing.

The printed manual's "one smartphone" line is about **scheduling ownership**, not a hard lock on manual GATT writes. The physical dial can still disable onboard schedules; that does not block these manual `f006` writes.

## Suggested PR surface

1. Accept exact aliases `Tap Timer` and `BX2` (optional `HOLMAN_ACCEPTED_ALIASES` adds more exact names).
2. Discover and connect with the same service UUID list (defaults include the on-air BX2 UUID CO3011 / `c521f000-...`). `HOLMAN_SERVICE_UUIDS` or `service_uuids=` fully replaces the list when set.
3. Unlock `c001` with `AE 8E` when the characteristic exists.
4. `start(runtime, zone=1)` writes `[0x01, tap_index, 0, mins]` (tap index `0x00` zone 1 / `0x01` zone 2); `stop()` writes zeros.
5. CLI `--start` / `--stop` / `--minutes` / `--zone`.
6. README mention of BTX2 / BX2 and a link here.

Leave Home Assistant bindings, retries, and site addresses out of the SDK.

## CLI

```
holmanctl --discover
holmanctl --start AA:BB:CC:DD:EE:FF --minutes 2 --zone 1
holmanctl --stop AA:BB:CC:DD:EE:FF
```
67 changes: 67 additions & 0 deletions docs/homeassistant-example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Home Assistant / Lovelace example (generic)

Zone 1 = tap index 0. Zone 2 = tap index 1. One outlet at a time. No site IPs or MACs.

f006 start bytes used by this SDK:

| Tap index | Zone | Start | Hex |
| --- | --- | --- | --- |
| 0 | Zone 1 | `[0x01, 0x00, 0x00, minutes]` | `010000NN` |
| 1 | Zone 2 | `[0x01, 0x01, 0x00, minutes]` | `010100NN` |
| — | Stop | `[0x00, 0x00, 0x00, 0x00]` | `00000000` |

## Package snippet

```yaml
holman_bt:
- mac: AA:BB:CC:DD:EE:FF
name: Holman BX2
default_runtime: 5
```

## Lovelace cards

Entities are named from the device name plus the zone
(`switch.holman_bx2_zone_1`, `switch.holman_bx2_zone_2`).

```yaml
type: vertical-stack
cards:
- type: entities
title: Holman BX2
entities:
- entity: number.holman_bx2_runtime
name: Minutes
- entity: switch.holman_bx2_zone_1
name: Zone 1
icon: mdi:water
- entity: switch.holman_bx2_zone_2
name: Zone 2
icon: mdi:water
- type: horizontal-stack
cards:
- type: button
name: Start Zone 1
icon: mdi:water
tap_action:
action: call-service
service: holman_bt.start
data:
zone: 1
minutes: 5
- type: button
name: Start Zone 2
icon: mdi:water
tap_action:
action: call-service
service: holman_bt.start
data:
zone: 2
minutes: 5
- type: button
name: Stop
icon: mdi:water-off
tap_action:
action: call-service
service: holman_bt.stop
```