diff --git a/README.md b/README.md deleted file mode 100644 index 3fa413f..0000000 --- a/README.md +++ /dev/null @@ -1,136 +0,0 @@ -# Holman Python SDK -[Holman CO3015](https://www.holmanindustries.com.au/products/bluetooth-tap-timer-co3015/) -and [BTX1](https://www.holmanindustries.com.au/products/btx1-tap-mounted-smart-valve/) are Bluetooth tap timers made by [Holman](https://www.holmanindustries.com.au/). - -The Holman Python SDK for Linux allows you to integrate your Holman(s) into any type of Linux application or script that can execute Python code. - -## Prerequisites -The Holman SDK requires [Python 3.4+](https://www.python.org) and a recent installation of [BlueZ](http://www.bluez.org/). It is tested to work fine with BlueZ 5.44, slightly older versions should however work, too. - -## Installation -These instructions assume a Debian-based Linux. - -On Linux the [BlueZ](http://www.bluez.org/) library is necessary to access your built-in Bluetooth controller or Bluetooth USB dongle. Some Linux distributions provide a more up-to-date BlueZ package, some other distributions only install older versions that don't implement all Bluetooth features needed for this SDK. In those cases you want to either update BlueZ or build it from sources. - -### Updating/installing BlueZ via apt-get - -1. `bluetoothd --version` Obtains the version of the pre-installed BlueZ. `bluetoothd` daemon must run at startup to expose the Bluetooth API via D-Bus. -2. `sudo apt-get install --no-install-recommends bluetooth` Installs BlueZ -3. If the installed version is too old, proceed with next step: [Installing BlueZ from sources](#installing-bluez-from-sources) - -### Installing BlueZ from sources - -The `bluetoothd` daemon provides BlueZ's D-Bus interfaces that is accessed by the Holman SDK to communicate with Holman Bluetooth tap timers. The following commands download BlueZ 5.44 sources, built them and replace any pre-installed `bluetoothd` daemon. It's not suggested to remove any pre-installed BlueZ package as its deinstallation might remove necessary Bluetooth drivers as well. - -1. `sudo systemctl stop bluetooth` -2. `sudo apt-get update` -3. `sudo apt-get install libusb-dev libdbus-1-dev libglib2.0-dev libudev-dev libical-dev libreadline-dev libdbus-glib-1-dev unzip` -4. `cd` -5. `mkdir bluez` -6. `cd bluez` -7. `wget http://www.kernel.org/pub/linux/bluetooth/bluez-5.44.tar.xz` -8. `tar xf bluez-5.44.tar.xz` -9. `cd bluez-5.44` -10. `./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --enable-library` -11. `make` -12. `sudo make install` -13. `sudo ln -svf /usr/libexec/bluetooth/bluetoothd /usr/sbin/` -14. `sudo install -v -dm755 /etc/bluetooth` -15. `sudo install -v -m644 src/main.conf /etc/bluetooth/main.conf` -16. `sudo systemctl daemon-reload` -17. `sudo systemctl start bluetooth` -18. `bluetoothd --version` # should now print 5.44 - -Please note that some distributions might use a different directory for system deamons, apply step 13 only as needed. - -### Enabling your Bluetooth adapter - -1. `echo "power on" | sudo bluetoothctl` Enables your built-in Bluetooth adapter or external Bluetooth USB dongle - -### Using BlueZ commandline tools -BlueZ also provides an interactive commandline tool to interact with Bluetooth devices. You know that your BlueZ installation is working fine if it discovers any Bluetooth devices nearby. - -`sudo bluetoothctl` Starts an interactive mode to talk to BlueZ - * `power on` Enables the Bluetooth adapter - * `scan on` Start Bluetooth device scanning and lists all found devices with MAC addresses - * `connect AA:BB:CC:DD:EE:FF` Connects to a Holman tap timer with specified MAC address - * `exit` Quits the interactive mode - -### Installing Holman Python SDK - -To install Holman module and the Python3 D-Bus dependency globally, run: - -``` -sudo pip3 install holman -sudo apt-get install python3-dbus -``` - -#### Running the Holman control script - -To test if your setup is working, run the following command. Note that it must be run as root because on Linux, Bluetooth discovery is a restricted operation. - -``` -sudo holmanctl --discover -sudo holmanctl --connect AA:BB:CC:DD:EE:FF # Replace the MAC address with your Holman's MAC address -sudo holmanctl --help # To list all available commands -``` - -## SDK Usage - -### Discovering nearby Holman tap timers - -The SDK entry point is the `TapTimerManager` class. Check the following example to dicover any Holman tap timer nearby. - -Please note that communication with your Bluetooth adapter happens over BlueZ's D-Bus API, hence an event loop needs to be run in order to receive all Bluetooth related events. You can start and stop the event loop via `run()` and `stop()` calls to your `TapTimerManager` instance. - - -```python -import holman - -class TapTimerManagerPrintListener(holman.TapTimerManagerListener): - def tap_timer_discovered(self, tap_timer): - print("Discovered Holman tap_timer", tap_timer.mac_address) - -manager = holman.TapTimerManager(adapter_name='hci0') -manager.listener = TapTimerManagerPrintListener() -manager.start_discovery() -manager.run() -``` - -### Connecting to a Holman tap timer and receiving user input events - -Once `TapTimerManager` has discovered a Holman tap timer you can use the `TapTimer` object(s) that you retrieved from `TapTimerManager.tap_timers()` to connect to it. Alternatively you can create a new instance of `TapTimer` using the name of your Bluetooth adapter (typically `hci0`) and Holman's MAC address. - -Make sure to assign a `TapTimerListener` object to the `listener` attribute of your TapTimer instance. It will notify you about all Holman tap timer related events such connection, disconnection and user input events. - -The following example connects to a Holman tap timer manually: - -```python -import holman - -manager = holman.TapTimerManager(adapter_name='hci0') - -tap_timer = holman.TapTimer(mac_address='AA:BB:CC:DD:EE:FF', manager=manager) -tap_timer.listener = holman.TapTimerListener() # Use an instance of your own holman.TapTimerListener subclass -tap_timer.connect() - -manager.run() -``` - -As with Holman tap timer discovery, remember to start the Bluetooth event loop with `TapTimerManager.run()`. - -### Start the tap running - -Once a Holman tap timer is connected you can start the tap with `TapTimer.start(runtime=1)`. Pass this a runtime (in minutes) for how long to run the tap. - -## Support - -Please open an issue. - -## Contributing - -Contributions are welcome via pull requests. Please open an issue first in case you want to discus your possible improvements to this SDK. - -## License - -The Holman Python SDK is available under the MIT License. diff --git a/docs/BX2.md b/docs/BX2.md new file mode 100644 index 0000000..c1f0f70 --- /dev/null +++ b/docs/BX2.md @@ -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 `. 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 +``` diff --git a/docs/homeassistant-example.md b/docs/homeassistant-example.md new file mode 100644 index 0000000..f098d56 --- /dev/null +++ b/docs/homeassistant-example.md @@ -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 +```