The Calor bt app doesn't allow downgrading, and pairing gets in the way in all versions anyway. This repo fixed that.
Recommended: Flash v1.48 with --noauth to remove all pairing requirements. This not only eliminates the PIN prompt that causes connection issues with Home Assistant, but also any pairing requiremts whatsoever.
If you use it from home assistant, ssh into it and do everything from there
- Disable the device in Home Assistant to avoid interference
- Make sure nothing is connected to the device (bluetoothctl, HA, phone)
- If the device shows "Ins", press the dial and wait for "AdA" to finish
- Pair if needed:
$ bluetoothctl
> scan on
> pair <addr> # (accept prompt or enter 6-digit PIN from display, if you can't pair, see unbrick_flash.py below)
> disconnect <addr>
> quit# Clone this repo
git clone https://github.com/dbuezas/eq3-flashing.git /tmp/eq3-flashing
cd /tmp/eq3-flashing
# Create a venv and install dependencies
python3 -m venv /tmp/eq3-venv
/tmp/eq3-venv/bin/pip install bleak
# Flash MCU + BLE with noauth (auto-detects variant)
# Replace <address> with your device's MAC (e.g. 00:1A:22:12:B6:05)
/tmp/eq3-venv/bin/python3 flash_firmware.py <address> 1.48 --noauthIf pairing fails or you don't know the PIN, use the Bumble method instead (see Method 2).
After flashing, power cycle the thermostat (remove/reinsert batteries) and restart your HA Bluetooth integration.
Three methods to flash firmware:
| Method | Script | Requires | Use case |
|---|---|---|---|
| BLE OTA | flash_firmware.py |
bleak, paired or noauth FW | Recommended — normal flash (MCU + BLE) |
| BLE OTA (Bumble) | unbrick_flash.py |
bumble, root, Linux | Can't pair / don't know PIN |
| UART (PUART) | uart_eeprom.py |
USB-UART adapter, physical access | Bricked device recovery, EEPROM dump, MAC change |
Two hardware variants exist. The mcu firmwares are different (unknown in what) but either works in both. The ble firmwares are functionally identical — only the BLE device name and OTA App ID differ:
| Variant | Device name | OTA App ID |
|---|---|---|
| CC-RT-BLE | CC-RT-BLE |
0x1000 |
| CC-RT-M-BLE | CC-RT-M-BLE |
0x1001 |
The App ID is never validated — either firmware variant works on either hardware. The correct variant is auto-detected from the device name, or can be forced with --variant.
| Version | BLE size (BLE/M) | MCU size | PIN | Notes |
|---|---|---|---|---|
| 1.05 | 17118 / — | 33712 | None | Initial release (2015), CC-RT-BLE only |
| 1.06 | 17118 / — | 33236 | None | Connection fixes, CC-RT-BLE only |
| 1.10 | 18270 / 18274 | 33088 | None | Week program, extended status (2016) |
| 1.20 | 18490 / 18512 | 33088 | None | Presets in status response (2018) |
| 1.46 | 19120 / 19124 | 34024 | 6-digit | Real BLE pairing, passkey on LCD (2020) |
| 1.48 | 19128 / 19132 | 34024 | 6-digit | Mandatory passkey for all connections (2024) |
The MCU .enc files are AES-256-CBC encrypted. Recovery of the key,
IV, OTA wire format, RAM↔flash byte rotation, and pre-decrypted
plaintexts of all 10 firmware variants are documented in
stm8-aes-key.md, with the decrypted streams
checked into decrypted-stm8/.
There's also an open challenge: the OTA bootloader will accept forged
firmware (returns FD A1 commit) but a separate boot-time integrity
check refuses to actually run modified code. Cracking that gate is
unsolved — see the writeup if you want to take a swing at it.
firmware/<version>/ble_CC-RT-BLE.bin # BLE firmware (original)
firmware/<version>/ble_CC-RT-M-BLE.bin # BLE firmware M variant (original)
firmware/<version>/noauth/ble_CC-RT-BLE.bin # BLE firmware (no-pairing patch)
firmware/<version>/noauth/ble_CC-RT-M-BLE.bin
firmware/<version>/mcu_CC-RT-BLE.enc # MCU firmware (AES encrypted)
firmware/<version>/mcu_CC-RT-M-BLE.enc
Patched BLE firmware that removes all pairing requirements. Works on all firmware versions.
Changes:
- CCCD permission
0x6E→0x2E(clearsAUTH_WRITABLEbit, keeps all others intact) - v1.46+:
encr_required0x03→0x00(disables SMP Security Request)
Use --noauth flag with any flash script.
Standard flash via Bluetooth. Requires the device to be paired (or already running noauth firmware).
pip install bleak
# Show available firmware versions
python3 flash_firmware.py
# Scan for devices
python3 flash_firmware.py scan
# Flash both MCU and BLE (auto-detects variant)
python3 flash_firmware.py <address> <version>
# Flash with noauth BLE firmware
python3 flash_firmware.py <address> <version> --noauth
# Flash BLE only (skip MCU)
python3 flash_firmware.py <address> <version> --ble-only --noauth
# Force variant
python3 flash_firmware.py <address> <version> --variant CC-RT-M-BLE
# Specify adapter (Linux)
python3 flash_firmware.py <address> <version> --adapter hci0Flash order: MCU first, then BLE. The script automatically reconnects between steps.
Uses Google's Bumble library to bypass Linux kernel SMP enforcement. No PIN or pairing required. Use this when you can't pair the device (unknown PIN, locked out, mismatched firmware).
pip install bumble
# Flash BLE firmware (bypasses PIN)
sudo python3 unbrick_flash.py <address> 1.48 --noauth
# With variant and adapter
sudo python3 unbrick_flash.py <address> 1.48 --noauth --variant CC-RT-BLE --adapter hci0Requirements:
- Linux only (raw HCI socket)
- Root access (or
CAP_NET_RAW) - Temporarily takes exclusive control of the BLE adapter
How it works:
- Bumble talks directly to the HCI adapter, bypassing the kernel's BLE SMP stack
- The thermostat sends an SMP Security Request after connection, but Bumble ignores it
- The thermostat accepts GATT operations anyway — pairing was never truly enforced at the BLE level
Note: This script only flashes BLE firmware. To also update the MCU, first flash BLE with --noauth via Bumble, then use flash_firmware.py <address> 1.48 --noauth --mcu-only (MCU flash goes through the BLE chip, so it needs pairing or noauth firmware).
Direct EEPROM read/write via the PRG2 programming header on the PCB. Does not require BLE at all. Used for:
- Recovering GATT DB bricked devices (corrupted permissions, no OTA service visible)
- Dumping full EEPROM contents for analysis
- Patching individual bytes (tested, safe)
- Changing the BLE MAC address
PRG2 header pinout (5 pins, left to right when text is readable):
Pin 1: 3.3V
Pin 2: GND
Pin 3: PUART RX (input to BLE chip)
Pin 4: PUART TX (output from BLE chip, 115200 baud)
Pin 5: VCC
Connect a 3.3V USB-UART adapter:
- Adapter TX → PRG2 pin 3
- Adapter RX → PRG2 pin 4
- Adapter GND → PRG2 pin 2
The BCM20736 ROM has a built-in HCI download mode accessible via PUART:
- Script continuously sends HCI Reset commands
- Pull the batteries from the thermostat, wait 2 seconds
- Re-insert batteries — the ROM bootloader responds before the application starts
- Script uploads a minidriver to RAM (enables EEPROM access)
- EEPROM can be read/written via indirect memory map
Offset Size Purpose
0x0000 256B SS1 (Static Section 1 — active flag, config)
0x0100 256B SS2 (Static Section 2 — backup)
0x0140 ~1KB VS (Volatile Section — NVRAM, pairing data)
0x0580 ~31KB DS1 (Data Section 1 — active firmware image)
0x8000 ~31KB DS2 (Data Section 2 — backup/OTA staging)
pip install pyserial
# Dump full EEPROM (64KB) — pull battery, wait 2s, re-insert when prompted
python3 uart_eeprom.py dump -p /dev/ttyUSB0 -o eeprom_backup.bin
# Read a specific region
python3 uart_eeprom.py read -p /dev/ttyUSB0 --offset 0x3C00 --length 128
# Patch a single byte (e.g., fix corrupted GATT DB permission) — TESTED, SAFE
python3 uart_eeprom.py patch -p /dev/ttyUSB0 --offset 0x3C2A --value 0x8A
# Change the BLE MAC address — TESTED, SAFE (single-byte writes)
python3 uart_eeprom.py patch-mac -p /dev/ttyUSB0 --mac 00:1A:22:12:B6:05
# Flash BLE firmware to DS1 — use with care, always dump a backup first
python3 uart_eeprom.py flash-fw -p /dev/ttyUSB0 -i firmware/1.48/noauth/ble_CC-RT-BLE.bin
# Flash full EEPROM image — use with care, always dump a backup first
python3 uart_eeprom.py flash -p /dev/ttyUSB0 -i eeprom_backup.binflash-fw writes a BLE firmware .bin file to the DS1 region only (the firmware area). It preserves device identity (MAC, pairing data, NVRAM). Use this to install or recover BLE firmware without affecting anything else.
flash writes a full 64 KB EEPROM image, overwriting everything — firmware, MAC address, pairing data, NVRAM. If flashing a dump from a different device, use patch-mac afterwards to restore the original MAC address.
Be careful with both — bulk EEPROM writes can corrupt the firmware header, making the device unrecoverable via PUART. Always dump a backup first. flash-fw has been used successfully to recover devices, but verification may report false failures due to UART timing. Prefer patch / patch-mac for targeted fixes, or flash via BLE OTA when possible.
# 1. Dump current EEPROM as backup
python3 uart_eeprom.py dump -p /dev/ttyUSB0 -o my_backup.bin
# 2. Flash the provided noauth v1.48 EEPROM image (use the correct variant)
python3 uart_eeprom.py flash -p /dev/ttyUSB0 -i firmware/1.48/eeprom_CC-RT-BLE_noauth.bin
# or for M variant:
# python3 uart_eeprom.py flash -p /dev/ttyUSB0 -i firmware/1.48/eeprom_CC-RT-M-BLE_noauth.bin
# 3. Restore your device's original MAC address
python3 uart_eeprom.py patch-mac -p /dev/ttyUSB0 --mac 00:1A:22:XX:XX:XX
# 4. Power cycle, then flash MCU firmware to match via BLE (noauth is already on)
python3 flash_firmware.py 00:1A:22:XX:XX:XX 1.48 --noauth --mcu-onlyIf you only need to replace the BLE firmware (e.g., corrupted GATT DB) without touching the device identity (MAC, pairing data, NVRAM):
# 1. Dump current EEPROM as backup
python3 uart_eeprom.py dump -p /dev/ttyUSB0 -o my_backup.bin
# 2. Flash BLE firmware only to DS1
python3 uart_eeprom.py flash-fw -p /dev/ttyUSB0 -i firmware/1.48/noauth/ble_CC-RT-BLE.bin
# 3. Power cycle, then flash MCU firmware to match via BLE
python3 flash_firmware.py 00:1A:22:XX:XX:XX 1.48 --noauth --mcu-onlyAfter any operation, power cycle the device (remove and re-insert batteries) to resume normal operation.
The minidriver is a small program uploaded to RAM that enables EEPROM access:
firmware/minidriver/
uart_20736.hex # Standard minidriver (RAM read/write only)
uart_DISABLE_EEPROM_WP_PIN1.hex # WP-disable minidriver (required for EEPROM access)
The WP-disable variant is required — the standard minidriver cannot access the EEPROM.
Known-good EEPROM dumps (64KB, noauth v1.48 firmware):
firmware/1.48/eeprom_CC-RT-BLE_noauth.bin
firmware/1.48/eeprom_CC-RT-M-BLE_noauth.bin
If you flash one device's EEPROM dump onto a different device, remember to change its mac address after.
- Thermostat appears to work normally (display, motor, buttons)
- BLE advertising works (device is discoverable)
- BLE connection works, but only 3 GATT services visible (GAP, GATT, Device Info)
- Thermostat service and OTA service are missing
- Cannot flash via BLE OTA
- PUART still works (debug trace visible on PRG2 pin 4)
Corrupted GATT DB permission bytes. If bits 0 (VARIABLE_LENGTH) or 7 (SERVICE_UUID_128) are changed, the GATT DB parser misreads entry header sizes, shifting all subsequent entries. The OTA service becomes invisible.
Option A: UART EEPROM patch (safest — single-byte writes, tested and proven)
# Connect UART adapter to PRG2 header
# Dump EEPROM for analysis
python3 uart_eeprom.py dump -p /dev/ttyUSB0 -o broken.bin
# Compare with known-good dump or the original firmware file to find corrupted bytes
# Patch the specific bytes (one at a time — safe, proven)
python3 uart_eeprom.py patch -p /dev/ttyUSB0 --offset 0x3C2A --value 0x8A
python3 uart_eeprom.py patch -p /dev/ttyUSB0 --offset 0x3C68 --value 0x8AOption B: Bumble BLE flash (if OTA service is still visible but PIN is unknown)
sudo python3 unbrick_flash.py <address> 1.48 --noauth --variant CC-RT-BLEBulk EEPROM writes via PUART carry risk. A partial write failure can corrupt the DS1 firmware header, causing the SPAR app to not load. Without the SPAR app, PUART is never initialized. Recovery via the inter-chip UART (STM8 PA2/PA3 = pins 3/4, holding NRST to GND) was attempted but did not work — the ROM did not respond to HCI Reset at 115200 on those pins. The device becomes permanently bricked.
flash-fw has been used successfully to recover devices, but verification may fail even on a successful write (known issue — UART timing sensitivity). Always dump a backup first.
The patch and patch-mac commands (single-byte writes) are the safest write operations.
If someone finds a way to enter ROM download mode via the inter-chip UART:
- STM8 pin 2 (NRST/PA1) → GND (hold MCU in reset)
- STM8 pin 3 (PA2) → UART adapter TX (connects to BCM20736 RXD, pin 12)
- STM8 pin 4 (PA3) → UART adapter RX (connects to BCM20736 TXD, pin 13)
- Pins 2, 3, 4 are adjacent on the top-left corner of the 48-pin QFP
PRG2 pin 4 outputs debug trace at 115200 baud. Connect UART RX only to monitor:
Pack ADV Field perform # Every ~1s during advertising
GPIO-Interrupt detected # Button press
Transmission of PAIRING detected # MCU sent pairing command
trv_ble_connection_up: <addr> # BLE connection established
connection DOWN handler started! # BLE disconnect
CRC-Check successful # UART frame from MCU verified
Useful for diagnosing connection issues, verifying MCU↔BLE communication, and understanding device state.
