Metadata-Version: 2.5
Name: tenways-ble
Version: 0.2.1
Summary: Read-only BLE client, King Meter protocol and diagnostic CLI for TENWAYS e-bikes
Project-URL: Homepage, https://github.com/Styx85/tenways-ble
Project-URL: Issues, https://github.com/Styx85/tenways-ble/issues
Author: Christoph Berliner
License: MIT
License-File: LICENSE
Keywords: ble,bluetooth,ebike,home-assistant,king-meter,tenways
Requires-Python: >=3.11
Requires-Dist: bleak>=0.22
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Description-Content-Type: text/markdown

<p align="center"><img src="https://raw.githubusercontent.com/Styx85/tenways-ble/main/docs/icon.png" alt="" width="160"></p>

# tenways-ble

Small experimental Python library and CLI for local BLE access to TENWAYS e-bikes.

Read-only by design:

- discovers nearby TENWAYS BLE devices;
- decodes the proprietary **King Meter** frame protocol (CRC-16/MODBUS, TLV fields) used by the TENWAYS Longtail Duo;
- reads live telemetry: traction battery, odometer, trip, max/average speed, ride time;
- reads firmware and serial number;
- reads the standard Bluetooth Battery Level characteristic (`0x2A19`), which is **not** the traction battery;
- does **not** send any write, lock, light or configuration commands.

The protocol was reconstructed from an Android HCI capture of the official TENWAYS app; all test vectors are the original captured frames. Live operation against the bike is still being validated.

## Library usage

```python
import asyncio
from tenways_ble import TenwaysBike

async def main():
    async with TenwaysBike("E8:88:6C:31:B3:0D") as bike:
        status = await bike.read_status()
        print(status.battery_percent, status.odometer_km)

asyncio.run(main())
```

`TenwaysBike` also accepts an already connected `BleakClient` (for example from `bleak-retry-connector` in Home Assistant). It then only speaks the protocol and leaves the connection to the caller.

The sans-I/O protocol module `tenways_ble.protocol` builds and parses frames without any Bluetooth dependency.

## Devices

| Device | Advertised name | Notes |
|---|---|---|
| Longtail Duo drive | `Longtail Duo` | King Meter service, telemetry source. Standard battery reads ~1 %, not the traction battery. |
| Separate BLE device | `Tenways-ble-XXXXXX` | HID service, standard battery 100 %, also exposes the King Meter service. Function unconfirmed (remote/control unit?). |

## Known GATT layout

Standard battery service:

- Service: `0000180f-0000-1000-8000-00805f9b34fb`
- Battery Level: `00002a19-0000-1000-8000-00805f9b34fb` (`READ`, `NOTIFY`)

King Meter service (`43480001-…` contains `KING METER` in ASCII):

- Service: `43480001-f001-4b49-4e47-204d45544552`
- Write: `43480002-f001-4b49-4e47-204d45544552` (`WRITE WITHOUT RESPONSE`)
- Notify: `43480003-f001-4b49-4e47-204d45544552` (`NOTIFY`)
- Read: `43480004-f001-4b49-4e47-204d45544552` (`READ`, no useful content observed)

## King Meter protocol

```
[TYPE] [00] [LEN] [CMD] [PAYLOAD…] [CRC_HI] [CRC_LO] [45]
```

| Element | Meaning |
|---|---|
| TYPE | `0x62` read, `0x63` write |
| `00` | constant header byte |
| LEN | length of CMD + PAYLOAD; frame length is LEN + 6 |
| CRC | CRC-16/MODBUS (`0xA001` reflected, init `0xFFFF`) over TYPE…PAYLOAD, **high byte first** |
| `45` | end marker |

A read request lists field keys; the reply returns `[key][size][value]` entries, big endian. Replies (54 bytes for status) exceed the default 20-byte notification payload and are reassembled.

Status request (`CMD 0x0B`), as sent by the app: `62 00 0c 0b 06 07 08 09 11 0a 0b 0c 0f 23 4f 84 91 45`

| Key | Size | Meaning | Status |
|---|---|---|---|
| `0x06` | 2 | current speed, 0.1 km/h | plausible, 0 while parked |
| `0x07` | 2 | max speed, 0.1 km/h | matches app |
| `0x08` | 2 | average speed, 0.1 km/h | matches app |
| `0x09` | 4 | odometer, 0.1 km | matches app |
| `0x0A` | 4 | ride time, s | matches app |
| `0x0B` | 2 | unknown (655 at 68 %; range in 0.1 km?) | open |
| `0x0C` | 4 | trip, 0.1 km | matches app |
| `0x0F` | 1 | traction battery, % | plausible |
| `0x11` | 1 | unknown | open |
| `0x23` | 2 | unknown | open |
| `0x4F` | 1 | unknown | open |

Device info request (`CMD 0x04`, keys `04 05 4f 70`): key `0x04` holds three version bytes (`2.20.16` observed; the app shows `1.4.0` elsewhere, so this may be another component), key `0x70` the ASCII serial number.

The app also writes a clock sync (`TYPE 0x63`, `CMD 0x01`, key `0x3C`). This library deliberately does not implement it.

## Install for development

```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
pip install -e '.[dev]'
```

On Linux, BlueZ and a working Bluetooth adapter are required.

## CLI

Scan for likely TENWAYS devices:

```bash
tenways scan
```

Read live telemetry (traction battery, odometer, …):

```bash
tenways status E8:88:6C:31:B3:0D
```

Read firmware and serial number:

```bash
tenways device-info E8:88:6C:31:B3:0D
```

Read the standard BLE battery percentage (not the traction battery):

```bash
tenways battery E8:88:6C:31:B3:0D
```

Show basic GATT information and battery level:

```bash
tenways info E8:88:6C:31:B3:0D
```

Subscribe to the proprietary notification characteristic:

```bash
tenways listen E8:88:6C:31:B3:0D
```

Stop with `Ctrl+C`.

## Optional HCI trace on Linux

For traffic generated by this library/CLI on the same Linux Bluetooth controller, BlueZ `btmon` is useful:

```bash
sudo btmon -i hci0 -w tenways.btsnoop
```

Then run the CLI in another shell. The resulting `tenways.btsnoop` file can be inspected with Wireshark.

This does **not** passively capture a BLE connection between an Android phone and the bicycle. For the original TENWAYS app, use Android HCI snoop logging or a dedicated BLE sniffer.

## Safety

`tenways-ble` 0.1.0 never writes arbitrary data to the proprietary TENWAYS write characteristic. Reverse engineering should start from observed traffic rather than guessed commands.

## License

MIT

Never publish a raw phone HCI log; it contains traffic and identifiers of other devices.

## Releasing

Releases are published to PyPI by GitHub Actions via trusted publishing when a `v*` tag is pushed. The tag must match `__version__`.

## Acknowledgements / Credits

Based on and inspired by [stein155/cgo600-bridge](https://github.com/stein155/cgo600-bridge), which provided valuable groundwork for understanding the proprietary TENWAYS telemetry protocol and its integration with Home Assistant.

Many thanks to the original author and contributors.

The Longtail Duo implementation was developed using independently captured Bluetooth HCI traffic from the official TENWAYS app. It uses the King Meter BLE transport rather than the Nordic UART Service used by the CGO600 bridge.
