# ESP32 "CYD" ThermoPro Bridge

A proof-of-concept bridge that reads temperature data from a **ThermoPro TP930** (among others) Bluetooth BBQ thermometer and ships it to a self-hosted HTTP API for storage and display.

## Architecture

```plain
ThermoPro TP930 (BLE)
        │
        ▼
thermopro_cli.py  ←── BLE reader (reverse-engineered protocol, Linux only)
        │
        ▼
  bridge_poc.py   ←── polls CLI, normalizes payload, POSTs to API
        │
        ▼
  receiver.php    ←── stores latest reading + NDJSON log, serves status page
```

## Components

### `thermopro_cli.py`

Local copy of the [thermopro-cli](thermopro-cli/) tool. Communicates directly with the ThermoPro thermometer over BLE using a reverse-engineered protocol. Outputs probe temperatures, battery level, and unit as JSON.

### `bridge_poc.py`

Polling bridge that:

1. Calls `thermopro_cli.py` on a configurable interval
2. Normalizes the raw CLI output into a structured payload
3. POSTs the reading to the configured HTTP endpoint with a shared secret token

### `receiver.php`

Minimal PHP API server that:

- `POST /api/thermopro/readings` — accepts and validates a reading, writes it to `data/latest.json` and appends to `data/readings.ndjson`
- `GET /api/thermopro/latest` — returns the most recent reading as JSON
- `GET /` or `GET /status` — HTML status page (auto-refreshes every 10 seconds)

## Data Format

Each reading payload looks like:

```json
{
  "deviceId": "tp930-smoker",
  "device": "ThermoPro TP930",
  "mac": "C9:48:1D:B1:E1:E7",
  "connected": true,
  "battery": 90,
  "unit": "F",
  "probeCount": 4,
  "probes": [
    { "id": 1, "name": "Probe 1", "temperature": 266.2, "connected": true },
    { "id": 2, "name": "Probe 2", "temperature": null, "connected": false },
    { "id": 3, "name": "Probe 3", "temperature": null, "connected": false },
    { "id": 4, "name": "Probe 4", "temperature": null, "connected": false }
  ],
  "readingTime": "2026-06-06T12:00:00+00:00",
  "bridgeTime": "2026-06-06T12:00:01+00:00",
  "source": "thermopro_cli"
}
```

## Requirements

- **Bridge host:** Linux with Bluetooth (BlueZ), Python 3.8+, `bleak` library
- **API server:** PHP 8.x with write access to the `data/` directory

## Setup

### 1. Install Python dependencies

```bash
pip install bleak
```

### 2. Configure the receiver

Deploy `receiver.php` to a PHP-capable web server. Set the `THERMOPRO_BRIDGE_TOKEN` environment variable (or it defaults to `dev-secret`):

```bash
export THERMOPRO_BRIDGE_TOKEN="your-secret-token"
```

### 3. Run the bridge

```bash
python bridge_poc.py \
  --cli thermopro_cli.py \
  --endpoint https://your-server/api/thermopro/readings \
  --token your-secret-token \
  --device-id tp930-smoker \
  --mac C9:48:1D:B1:E1:E7 \
  --interval 15
```

**Options:**

| Flag | Default | Description |
| --- | --- | --- |
| `--cli` | `thermopro_cli.py` | Path to `thermopro_cli.py` |
| `--endpoint` | `http://127.0.0.1:8000/api/thermopro/readings` | API endpoint |
| `--token` | `dev-secret` | Shared token (`X-Bridge-Token` header) |
| `--device-id` | `tp930-smoker` | Stable identifier for this device |
| `--mac` | `C9:48:1D:B1:E1:E7` | ThermoPro BLE MAC address |
| `--interval` | `15` | Polling interval in seconds |
| `--once` | — | Read and POST once, then exit |

### Finding your device's MAC address

```bash
python thermopro_cli.py scan
```

## Data Storage

| File | Contents |
| --- | --- |
| `data/latest.json` | Most recent reading (overwritten each poll) |
| `data/readings.ndjson` | Append-only log of all readings (newline-delimited JSON) |

## Security

- The API endpoint is protected by a shared secret sent as the `X-Bridge-Token` request header.
- Use HTTPS for the endpoint in production to keep the token confidential.
- The token is compared with `hash_equals()` to prevent timing attacks.
