Python 3.10 – 3.14 Protocol Spec v3.9 204 Automated Tests RS485 Half-Duplex CCITT-16

Heatmiser V3 Protocol & Library Documentation

A definitive reference and developer guide for communicating with Heatmiser V3 network thermostats over RS485 and UH1 bridges. Replicating the official heatmiser_v3_protocol_3.9.pdf specification with enhanced clarity, exact byte offsets, and interactive tools.

1. Overview & Architecture

The heatmiserv3 Python library provides a robust, low-level and high-level interface for controlling and reading telemetry from Heatmiser thermostats communicating via the Heatmiser Version 3 RS485 protocol.

Hardware Topology

Heatmiser thermostats communicate over a 2-wire, half-duplex RS485 serial bus operating at 4800 baud, 8 data bits, no parity, 1 stop bit. A central master node—typically a Heatmiser UH1 wiring centre or Ethernet/RS485 bridge—originates all bus sessions and forwards commands to individual thermostats (slaves) addressed [1, 32].

In modern home automation setups, the UH1 or RS485 converter connects either via a direct local serial device (e.g. /dev/ttyACM0) or a TCP socket server (e.g. socket://192.168.1.57:102). The heatmiserv3 library establishes a connection to this interface via HeatmiserUH1 and coordinates session arbitration, frame formatting, CRC calculation, and response parsing.

2. Protocol Capability Audit: PDF v3.9 vs. heatmiserv3 Library

Below is a comprehensive audit assessing every chapter and feature described in the 45-page heatmiser_v3_protocol_3.9.pdf against the current library implementation:

PDF Chapter Protocol Capability / Feature Status in Library Implementation Details & Notes
Sec 3.1 & 3.2 RS485 Master Command Framing & Slave Verification Fully Covered _hm_form_message, _hm_form_message_crc, and _hm_verify_message_crc_uk handle framing, variable payloads, and 7-byte write confirmations.
Sec 3.3 DT / DT-E / PRT / PRT-E DCB Configuration Fully Covered Full 0..147 DCB mapping in config.yml. High-level convenience helpers for target temp, floor max, sensor selection (modes 0–4), differential, temperature format, keypad lock, on/off state, holiday hours, temp hold, and RTC.
Sec 3.4 PRT-HW (Programmable Thermostat + Hot Water) Fully Covered Full 0..292 DCB mapping in config.yml. Dedicated PRTHWThermostat class providing hot water boost (addr 41), hot water state (index 36), heating state (index 35), and sensor selection enforcement (Mode 0 built-in only).
Sec 3.5 TM1 Timer & Countdown Mode Thermostat Fully Covered 100% covered via TM1Thermostat and validated with comprehensive automated test suite. Includes On/Off, Key lock, Holiday, Countdown (0–1800m), Override (0/1/2), RTC, and 5/2 & 7-day schedules.
Sec 3.6 HC-EN V3 (Heating & Cooling with Dew Point / RH) Partially Covered Full DCB mapping registered in config.yml. Library can read DCB and map labels. Dedicated heating/cooling setters (dew point, cooling limit) are not yet wrapped in a specialized class.
Sec 3.7 & 3.8 OTS (Outside Temp Sensor) & HTS (Humidity Sensor) Not Covered PDF defines read-only DCB for OTS (addr 81) and HTS (addrs 41–72). The library does not currently provide standalone sensor reader classes.
Sec 4 Hot Water & Timer Boost via RS485 Fully Covered Fully supported via PRTHWThermostat.set_hot_water_boost() (addr 41), TM1Thermostat.set_countdown_timer() (addr 26), and HeatmiserThermostat.set_temp_hold() (addr 26).
Sec 5 & 6 Wi-Fi Direct Thermostat Protocol (Port 8068 + PIN) Not Covered PDF describes single-zone Wi-Fi thermostats accepting 0x93 frames with 4-byte PINs over port 8068. The library currently targets RS485 / UH1 bridges.
Sec 7 Multi-Link / Netmonitor (NM) Hub Protocol Not Covered PDF specifies multi-zone aggregator frames (0x98, 0x99, 0xA8) and history logging. The library focuses on direct thermostat communication.
Sec 8 & App A CRC-16 CCITT (0xFFFF) Lookup Table Algorithm Fully Covered CRC16 implements the exact nibble-swapping bitwise lookup algorithm from the Heatmiser C reference code.

3. Supported Thermostats & Model Identification

Every Heatmiser thermostat returns its Model ID at DCB Index 4 during a full DCB read:

Model Name Model ID (Dec / Hex) Description & Heating Modes DCB Lengths
DT 0 (0x00) Digital Thermostat, non-programmable, room heating only. 36 bytes
DT-E 1 (0x01) Digital Thermostat with remote/floor sensor for electrical underfloor heating. 36 bytes
PRT 2 (0x02) Programmable Room Thermostat, 4 comfort levels per day. 64 bytes (5/2 mode) / 148 bytes (7-day mode)
PRT-E 3 (0x03) Programmable Room Thermostat with electrical floor sensor probe. 64 bytes (5/2 mode) / 148 bytes (7-day mode)
PRT-HW 4 (0x04) Programmable Room Thermostat with independent Hot Water time clock channel. 72 bytes (5/2 mode) / 293 bytes (7-day mode)
TM1 5 (0x05) Time Clock & Countdown Timer (Heating timer or hot water). 19 bytes (countdown) / 51 bytes (5/2) / 163 bytes (7-day)
HC-EN 7 (0x07) Heating & Cooling Thermostat with Relative Humidity and Dew Point sensors. 68 bytes (5/2 mode) / 152 bytes (7-day mode)
OTS / HTS Sensors Outside Temperature Sensor (addr 81) & Humidity Sensor (addrs 41–72). 11 bytes / 13 bytes

4. How the Library Works

4.1 Master Command Frame Structure

Whenever a command is issued by the library, a 10-byte (read) or (10 + n)-byte (write) bytecode frame is assembled:

Byte # Field Name Value / Expression Description
0 Destination Address 1 – 32 (or 0xFF) Thermostat slave ID. 0xFF (255) is broadcast.
1 Frame Length 10 (Read) or 10 + n (Write) Total length of this frame including the 2-byte CRC.
2 Source Address 0x81 (129) Master controller address (typically 0x81).
3 Function Code 0 = Read, 1 = Write 0x00 to read from DCB, 0x01 to write parameters.
4 – 5 Start DCB Address start & 0xFF, (start >> 8) & 0xFF 16-bit parameter starting address in Little-Endian format.
6 – 7 Read / Write Length len & 0xFF, (len >> 8) & 0xFF Number of bytes. 0xFFFF requests the full DCB for read.
8 .. (n+7) Payload Data n bytes The configuration bytes (omitted on read frames).
n+8 .. n+9 16-bit CRC crc_low, crc_high CCITT-16 checksum computed across bytes 0 to (n+7).

4.3 CRC-16 Checksum Algorithm (CCITT 0xFFFF)

The Heatmiser V3 protocol uses a 16-bit CRC algorithm (polynomial 0xA001, initial register state 0xFFFF) calculated across all frame bytes preceding the checksum. The calculated checksum is transmitted in Little-Endian format as two bytes: [Low Byte, High Byte].

Code Line Reference: Python Library Implementation

In this repository, the CRC algorithm is implemented in heatmiserv3/heatmiser.py (lines 26–96) within the CRC16 class.

Reference Implementations Across Languages
Click tab to view language implementation
heatmiserv3/heatmiser.py (Lines 26–96) Python 3
# heatmiserv3/heatmiser.py lines 26-96
class CRC16:
    """This is the CRC hashing mechanism used by the V3 protocol."""

    LookupHigh = [
        0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70,
        0x81, 0x91, 0xA1, 0xB1, 0xC1, 0xD1, 0xE1, 0xF1,
    ]
    LookupLow = [
        0x00, 0x21, 0x42, 0x63, 0x84, 0xA5, 0xC6, 0xE7,
        0x08, 0x29, 0x4A, 0x6B, 0x8C, 0xAD, 0xCE, 0xEF,
    ]

    def __init__(self):
        self.high = 0xFF
        self.low = 0xFF

    def extract_bits(self, val):
        """Extracts 4 bits, XORs message data, and does table lookups."""
        thisval = (self.high >> 4) ^ val
        self.high = ((self.high << 4) | (self.low >> 4)) & 0xFF
        self.low = (self.low << 4) & 0xFF
        self.high = (self.high ^ self.LookupHigh[thisval]) & 0xFF
        self.low = (self.low ^ self.LookupLow[thisval]) & 0xFF

    def update(self, val):
        """Updates the CRC value using bitwise operations."""
        self.extract_bits(val >> 4)    # High nibble first
        self.extract_bits(val & 0x0F)  # Low nibble

    def run(self, message):
        """Calculates a CRC-16 checksum, returning [low, high]."""
        for value in message:
            self.update(value)
        return [self.low, self.high]
Heatmiser V3 Protocol Spec (Appendix A) ANSI C
/* Heatmiser V3 Protocol Specification - Appendix A */
static const unsigned char CRC16_LookupHigh[16] = {
    0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70,
    0x81, 0x91, 0xA1, 0xB1, 0xC1, 0xD1, 0xE1, 0xF1
};
static const unsigned char CRC16_LookupLow[16] = {
    0x00, 0x21, 0x42, 0x63, 0x84, 0xA5, 0xC6, 0xE7,
    0x08, 0x29, 0x4A, 0x6B, 0x8C, 0xAD, 0xCE, 0xEF
};

static unsigned char CRC16_High = 0xFF;
static unsigned char CRC16_Low  = 0xFF;

void CRC16_Init(void) {
    CRC16_High = 0xFF;
    CRC16_Low  = 0xFF;
}

void CRC16_Update4Bits(unsigned char val) {
    unsigned char t = (CRC16_High >> 4) ^ val;
    CRC16_High = (unsigned char)((CRC16_High << 4) | (CRC16_Low >> 4));
    CRC16_Low  = (unsigned char)(CRC16_Low << 4);
    CRC16_High ^= CRC16_LookupHigh[t];
    CRC16_Low  ^= CRC16_LookupLow[t];
}

void CRC16_Update(unsigned char val) {
    CRC16_Update4Bits(val >> 4);   /* High nibble first */
    CRC16_Update4Bits(val & 0x0F); /* Low nibble */
}

void CRC16_Calculate(const unsigned char *data, size_t len, unsigned char *out_low, unsigned char *out_high) {
    CRC16_Init();
    for (size_t i = 0; i < len; ++i) {
        CRC16_Update(data[i]);
    }
    *out_low  = CRC16_Low;
    *out_high = CRC16_High;
}
Digital Twin / Web Implementation TypeScript / JavaScript
const CRC16_LOOKUP_HIGH = [
  0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70,
  0x81, 0x91, 0xA1, 0xB1, 0xC1, 0xD1, 0xE1, 0xF1
];
const CRC16_LOOKUP_LOW = [
  0x00, 0x21, 0x42, 0x63, 0x84, 0xA5, 0xC6, 0xE7,
  0x08, 0x29, 0x4A, 0x6B, 0x8C, 0xAD, 0xCE, 0xEF
];

export function calculateCRC16(data: Uint8Array | number[]): [number, number] {
  let high = 0xFF;
  let low = 0xFF;

  function update4Bits(val: number) {
    const t = ((high >> 4) ^ val) & 0x0F;
    high = (((high << 4) | (low >> 4)) ^ CRC16_LOOKUP_HIGH[t]) & 0xFF;
    low = ((low << 4) ^ CRC16_LOOKUP_LOW[t]) & 0xFF;
  }

  for (let i = 0; i < data.length; i++) {
    const byte = data[i];
    update4Bits((byte >> 4) & 0x0F); // High nibble first
    update4Bits(byte & 0x0F);        // Low nibble
  }

  return [low, high]; // Returns [Low Byte, High Byte]
}
Idiomatic Rust Implementation Rust
const CRC16_LOOKUP_HIGH: [u8; 16] = [
    0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70,
    0x81, 0x91, 0xA1, 0xB1, 0xC1, 0xD1, 0xE1, 0xF1,
];
const CRC16_LOOKUP_LOW: [u8; 16] = [
    0x00, 0x21, 0x42, 0x63, 0x84, 0xA5, 0xC6, 0xE7,
    0x08, 0x29, 0x4A, 0x6B, 0x8C, 0xAD, 0xCE, 0xEF,
];

pub struct HeatmiserCrc16 {
    high: u8,
    low: u8,
}

impl HeatmiserCrc16 {
    pub fn new() -> Self {
        Self { high: 0xFF, low: 0xFF }
    }

    fn update_4bits(&mut self, val: u8) {
        let t = ((self.high >> 4) ^ val) as usize;
        self.high = (self.high << 4) | (self.low >> 4);
        self.low <<= 4;
        self.high ^= CRC16_LOOKUP_HIGH[t];
        self.low ^= CRC16_LOOKUP_LOW[t];
    }

    pub fn update(&mut self, val: u8) {
        self.update_4bits(val >> 4);
        self.update_4bits(val & 0x0F);
    }

    pub fn calculate(data: &[u8]) -> [u8; 2] {
        let mut crc = Self::new();
        for &b in data {
            crc.update(b);
        }
        [crc.low, crc.high] // [Low Byte, High Byte]
    }
}
Go Standard Implementation Go
package heatmiserv3

var (
    crcLookupHigh = [16]byte{
        0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70,
        0x81, 0x91, 0xA1, 0xB1, 0xC1, 0xD1, 0xE1, 0xF1,
    }
    crcLookupLow = [16]byte{
        0x00, 0x21, 0x42, 0x63, 0x84, 0xA5, 0xC6, 0xE7,
        0x08, 0x29, 0x4A, 0x6B, 0x8C, 0xAD, 0xCE, 0xEF,
    }
)

// CalculateCRC16 computes the Heatmiser V3 CRC-16 checksum returning (low, high).
func CalculateCRC16(data []byte) (byte, byte) {
    high := byte(0xFF)
    low := byte(0xFF)

    update4Bits := func(val byte) {
        t := (high >> 4) ^ val
        high = (high << 4) | (low >> 4)
        low = low << 4
        high ^= crcLookupHigh[t]
        low ^= crcLookupLow[t]
    }

    for _, b := range data {
        update4Bits(b >> 4)    // High nibble first
        update4Bits(b & 0x0F)  // Low nibble
    }

    return low, high
}
⚡ Dynamic Live CRC-16 Calculator & Step Inspector
Dynamic Browser Execution

Enter arbitrary hex or decimal bytes (space-separated, comma-separated, or 0x-prefixed) or choose a protocol preset. The algorithm executes live in your browser and verifies parity against the C Reference and Python implementations.

Test Presets:
CRC Low Byte (Byte -2): 0xB0 (176)
CRC High Byte (Byte -1): 0xEB (235)
Combined 16-Bit Word: 0xEBB0 (60336)
✅ Exact Parity with Python & C Spec
🔍 View Detailed Step-by-Step Nibble Trace

5. Thermostat DCB Register Tables

The tables below replicate the exact register tables from the official Heatmiser V3.9 protocol specification, augmented with explicit Access Rights, Endianness Rules, and Value Scaling.

DT / DT-E / PRT / PRT-E Register Table (Section 3.3)

DT/DT-E stops at index 35 (36 bytes). PRT in 5/2 mode is 64 bytes; in 7-day mode is 148 bytes.

Unique Addr DCB Index Parameter Name Access Format & Endianness Default Protocol Description & Notes
00–1DCB LengthR2B Big-Endian64 / 148Total byte length of the DCB.
22Vendor IDR1 Byte000 = Heatmiser, 01 = OEM.
33Version & StatusR1 Byte (Bitmask)10Bits 0–6 = Version (10 = v1.0). Bit 7 = Floor limit state (1 = in floor limit).
44Model IDR1 Byte200 = DT, 01 = DT-E, 02 = PRT, 03 = PRT-E.
55Temperature FormatR/W1 Byte000 = °C, 01 = °F.
66Switch DifferentialR/W1 Byte1Hysteresis between switching: [1, 3] °C.
77Frost Protect ModeR1 Byte1Frost protection state: 0 = Disabled, 1 = Enabled.
88–9Calibration OffsetR/W2B Big-Endian0Temperature calibration offset.
1010Output DelayR/W1 Byte0Switching delay in minutes: [0, 15].
1111Thermostat AddressR1 Byte1Comms network address: [1, 32].
1212Key LimitR/W1 Byte0Up/down temperature adjustment limit: [0, 10] °C.
1313Sensor SelectionR/W1 Byte000=Built-in, 01=Remote air, 02=Floor, 03=Built-in+Floor, 04=Remote+Floor.
1414Optimum StartR/W1 Byte0Pre-heat anticipation: [0, 3] hours.
1515Rate of ChangeR/W1 Byte20Time in minutes to raise temperature by 1°C.
1616Program ModeR/W1 Byte000 = 5/2 Day mode, 01 = 7-Day mode.
1717Frost Protect TempR/W1 Byte12Frost target temperature: [7, 17] °C.
1818Set Room TempR/W1 Byte16Target temperature setpoint: [5, 35] °C.
1919Floor Max LimitR/W1 Byte28Maximum floor safety temperature: [20, 45] °C.
2020Floor Limit EnableR/W1 Byte100 = Disabled, 01 = Enabled.
2121On / Off StateR/W1 Byte000 = Off, 01 = On.
2222Key LockR/W1 Byte000 = Unlocked, 01 = Locked.
2323Run ModeR/W1 Byte000 = Heating (Normal), 01 = Frost Protection.
2424–25Holiday HoursR/W2B Read:BE / Write:LE0Caution: Read is High byte ahead; Write is Low byte ahead!
3226–27Temp Hold MinsR/W2B Read:BE / Write:LE0Hold override duration. Write 0 to cancel.
3428–29Remote Air TempR2B Big-Endian0xFFFFActual temp is value / 10. Returns 0xFFFF if no sensor.
3630–31Floor TempR2B Big-Endian0xFFFFActual temp is value / 10. Returns 0xFFFF if no sensor.
3832–33Built-In Air TempR2B Big-Endian—Actual temp is value / 10.
4034Error CodeR1 Byte0E0 = Built-in error, E1 = Floor sensor error, E2 = Remote air error.
4135Heating StateR1 Byte000 = No heating active, 01 = Heating currently calling.
4336–39Clock (RTC)R/W4 Bytes—[Week (1..7), Hour (0..23), Min (0..59), Sec (0..59)].
4740–51Weekday ComfortR/W12 Bytes—4 periods * 3 bytes (Hour, Min, Temp). Temp = 24 cancels period.
5952–63Weekend ComfortR/W12 Bytes—4 periods * 3 bytes (Hour, Min, Temp).
103..64–1477-Day ComfortR/W84 Bytes—Mon–Sun comfort levels (12 bytes per day). Unique addrs 103, 115, 127, 139, 151, 163, 175.

TM1 Timer & Countdown Register Table (Section 3.5)

Countdown mode finishes at index 18 (19 bytes); 5/2 mode is 51 bytes; 7-day mode is 163 bytes.

Unique Addr DCB Index Parameter Name Access Format & Endianness Default Protocol Description & Notes
00–1DCB LengthR2B Big-Endian19 / 51 / 163Total length of TM1 DCB based on program mode.
22Vendor IDR1 Byte000 = Heatmiser, 01 = OEM.
33VersionR1 Byte1010 = VER 1.0.
44Model IDR1 Byte5TM1 = 0x05.
115AddressR1 Byte1Slave address [1, 32].
166Program ModeR/W1 Byte000 = 5/2 mode, 01 = 7-day mode, 02 = Countdown timer mode.
—7ReservedR1 Byte0Not currently used.
218On / Off StateR/W1 Byte000 = Off, 01 = On.
229Key LockR/W1 Byte000 = Key Unlock, 01 = Key Lock.
2410–11Holiday HoursR/W2B Read:BE / Write:LE0Write 0 to cancel holiday. Low byte first when writing.
2612–13Countdown MinutesR/W2B Read:BE / Write:LE0Countdown duration: [0, 1800] minutes. Low byte first when writing.
4214Timer State / OverrideR/W1 Byte0Read: 00=Off, 01=On.
Write: 00=Auto (run program), 01=Override On, 02=Override Off.
4315–18Clock (RTC)R/W4 Bytes—[Week (1..7), Hour (0..23), Min (0..59), Sec (0..59)].
7119–34Weekday SwitchingR/W16 Bytes—4 on/off switching pairs: Time 1–4 On H/M, Off H/M. Hour 24 = Cancelled.
8735–50Weekend SwitchingR/W16 Bytes—4 on/off switching pairs: Time 1–4 On H/M, Off H/M.
187..28351–1627-Day SwitchingR/W112 Bytes—Monday (187), Tuesday (203), Wednesday (219), Thursday (235), Friday (251), Saturday (267), Sunday (283). 16 bytes each.

PRT-HW (Heating & Hot Water) Register Table (Section 3.4)

Model ID = 4 (0x04). Contains both 12-byte heating comfort levels and 16-byte hot water timer switching periods.

Unique Addr DCB Index Parameter Name Access Format Default Protocol Description & Notes
0..350..35Standard TelemetryRVar—Same registers as PRT (DCB len, Model 04, temp format, sensor, current temp).
4236Current Hot Water StateR/W1 Byte0Read: 00=Off, 01=On.
Write: 00=Auto program, 01=Override ON, 02=Override OFF.
4337–40Clock (RTC)R/W4 Bytes—Week 1..7, Hour 0..23, Min 0..59, Sec 0..59.
4741–52Weekday Heating ComfortR/W12 Bytes—4 periods * 3 bytes (Hour, Min, Temp). Hour 24 cancels.
5953–64Weekend Heating ComfortR/W12 Bytes—4 periods * 3 bytes (Hour, Min, Temp).
7165–80Weekday Hot Water TimerR/W16 Bytes—4 on/off pairs (Time 1..4 On H/M, Off H/M). Hour 24 cancels.
8781–96Weekend Hot Water TimerR/W16 Bytes—4 on/off pairs (Time 1..4 On H/M, Off H/M).
103..17597–1807-Day Heating ComfortR/W84 Bytes—7 days * 12 bytes = 84 bytes.
187..283181–2927-Day Hot Water TimerR/W112 Bytes—7 days * 16 bytes = 112 bytes.

HC-EN V3 (Heating & Cooling) Register Table (Section 3.6)

Model ID = 7 (0x07). 68 bytes in 5/2 mode, 152 bytes in 7-day mode.

Unique Addr DCB Index Parameter Name Access Format Default Protocol Description & Notes
44Model IDR1 Byte7HC-EN = 0x07.
2913Cooling EnableR/W1 Byte100 = Disabled, 01 = Enabled.
—14Dew Point DifferentialR/W1 Byte3Range: [0, 10] °C.
—15Dew Point Time LimitR/W1 Byte0Minutes: [0, 60].
2724Cooling SetpointR/W1 Byte24Cooling setpoint temperature.
2328Run ModeR/W1 Byte000 = Heating, 01 = Frost, 02 = Cooling.
3036Relative HumidityR1 Byte—Relative Humidity RH% reading from built-in/remote sensor.
2837Dew Point TempR1 Byte—Calculated dew point temperature.
4138Output State BitmaskR1 Byte (Bitmask)—bit0=Heat On, bit1=Heat Floor Limit, bit4=Cool On, bit5=Cool Floor Limit, bit6=Dew Point Reached, bit7=Dew Point Exceeded.

Wi-Fi System Variants (Section 6)

Single-zone Wi-Fi thermostats communicate over direct TCP sockets on port 8068 with PIN authentication.

Unique Addr DCB Index Parameter Name Access Format Description
317Away ModeR/W1 Byte00 = Normal mode, 01 = Away mode.
2410–14Holiday Return DateR/W5 Bytes[Year, Month, Day, Hour, Min]. Write single byte 00 to cancel.
2415Holiday StatusR1 Byte00 = Not in holiday, 01 = In holiday mode.

6. Visual Test Harness: Real-World DCB Dynamics

Writing register values to a Heatmiser thermostat over RS485 does not merely alter memory bytes in the Device Control Block (DCB)—it orchestrates physical relays, drives safety interlocks, updates LCD annunciators, and modulates HVAC heating loops in the real world.

🖥️ Multi-Screen Digital Twin Demo (Whole-Home Simulation)
Run the dedicated whole-home digital twin on a separate screen or monitor: 4x PRT-1 underfloor heating thermostats (Bedroom 1, Bedroom 2, Bathroom, Kitchen/Living), 1x TM1 Hot Water controller, an interactive floorplan with heating loops, connected to a Heatmiser UH1 8-zone wiring centre and an RS485 Raspberry Pi running Home Assistant. Includes multi-button hardware chord shortcuts (simulating simultaneous 2-finger presses).
🚀 Launch 5-Zone Twin (Separate Screen) →
Live Adjacent Digital Twin

In Split View (desktop default), the Virtual Thermostat Digital Twin is rendered adjacent to this guide on the right side of the screen. You can interact with tactile faceplate buttons, adjust physical environment temperatures, observe relay contacts opening and closing, and test safety cutouts in real time.

6.1 Heating Relay Switching & Differential Hysteresis

The central heating relay (connected across Terminals 1 & 2) switches 230V mains or dry-contact signals to central heating motorized zone valves and boiler call-for-heat terminals:

  • Call for Heat: When the ambient temperature drops below Target Setpoint - Switching Differential (DCB Addr 18 and Addr 27), the microcontroller fires Relay 1/2. The physical circuit closes, the boiler fires, and the 🔥 flame icon illuminates.
  • Setpoint Reached: When the ambient temperature reaches or exceeds the target setpoint, Relay 1/2 opens. The boiler shuts down and the flame icon extinguishes.
  • Hysteresis Protection: The 1°C, 2°C, or 3°C differential (DCB Addr 27) prevents rapid boiler short-cycling, extending compressor and burner life.

6.2 Underfloor Heating Overheat Thermal Safety Interlock

When configured for Mode 3 (Built-in Air + Floor Limit Sensor), the thermostat regulates room comfort using air temperature while continuously polling the NTC floor probe embedded in the floor screed:

  • Safety Cutout Threshold: If the floor temperature reaches or exceeds Floor Max Limit (DCB Addr 29, typically 28°C–35°C), the firmware trips an Emergency Thermal Safety Cutout.
  • Forced Relay Disengagement: The heating relay is forcibly opened immediately, even if the room is cold and demanding heat.
  • Real-World Protection: This interlock protects engineered wood flooring from buckling, adhesive breakdown, and cement screed cracking. The LCD flashes FLR HI or a warning badge.

6.3 Domestic Hot Water Dual-Channel Control (PRT-HW)

The PRT-HW model features an independent secondary relay across Terminals 3 & 4 dedicated to domestic hot water (DHW) cylinder valves:

  • Manual Hot Water Boost: Writing 01 to DCB Addr 41 (Unique Address 41) closes Terminals 3 & 4, energizing the cylinder valve and illuminating the 🚿 tap icon.
  • Holiday Suppression: When Holiday Mode is engaged, hot water heating is suppressed for the holiday duration to conserve energy while the property is unoccupied.

6.4 TM1 Time Clock & Countdown Timer Operations

The TM1 operates in three distinct programming modes (DCB Addr 16: 5/2 day, 7 day, or Countdown Timer):

  • Countdown Boost: Setting Countdown Minutes (DCB Addr 26–27, transmitted Little-Endian) energizes the relay and displays the remaining minutes alongside the ⏱️ timer icon. When the counter expires (0 minutes), the relay disengages automatically.
  • Timer Override: DCB Addr 42 allows software overrides (0 = Auto Program, 1 = Constant ON, 2 = Constant OFF).

6.5 Commercial Anti-Tamper Keypad Lockout

In public buildings, rental properties, and commercial installations, keypad locking prevents unauthorized temperature manipulation:

  • Lock Level 1: Full keypad lock (DCB Addr 22 = 1). All tactile front panel buttons are disabled, displaying a padlock 🔒 icon.
  • Tamper Alarm: Attempting to press buttons on the physical faceplate causes the display to flash a red LOC tamper warning and prevents changes.
  • Lock Level 2: Partial lock allowing setpoint adjustment within configured temperature limits.

6.6 Holiday Mode Setback vs. Sub-Zero Frost Protection

Energy conservation and freeze prevention are governed by two safety state machines:

  • Holiday Mode: Setting holiday hours (DCB Addr 24–25) suspends normal scheduled comfort levels and locks the target to the Frost Protect Temperature (DCB Addr 17, default 12°C). The 🧳 suitcase icon is displayed.
  • Standby Freeze Protection: Even when the user turns the thermostat completely OFF / Standby (DCB Addr 21 = 0), an emergency freeze-protection monitor remains active. If the room drops $\le 12^\circ\text{C}$, Relay 1/2 closes and the ❄️ snowflake icon illuminates to prevent burst water pipes.

6.7 One-Click Scenario Demonstration Presets

Click any scenario below to automatically configure the simulated environment, mutate the thermostat DCB registers, and observe real-world hardware actuation:

📉
1. Room Cools Below Setpoint (Call for Heat)
Ambient drops to 19°C (Setpoint 21°C). Watch Relay 1/2 close, 🔴 red LED light up, and 🔥 flame appear.
📈
2. Target Temperature Reached (Hysteresis Cutoff)
Ambient rises to 21.2°C. Watch Relay 1/2 open, flame extinguish, and boiler return to standby.
🚨
3. Underfloor Overheat Safety Interlock Cutout
Floor reaches 36°C (Max Limit 32°C). Relay FORCIBLY OPENS despite cold room (18°C) to protect flooring!
⏱️
4. TM1 Countdown Timer Boost (120 Minutes)
Switches to TM1 model, starts 120-min countdown timer, energizes timer relay, displays countdown.
🧳
5. 7-Day Holiday Mode Energy Setback
Sets 168 hours holiday. Normal schedule disabled, target forced to 12°C frost setback, suitcase icon displayed.
🔒
6. Commercial Anti-Tamper Keypad Lockout
Engages Lock Level 1. Padlock icon displayed on LCD. Front panel button presses trigger tamper alarms.
🚿
7. PRT-HW Domestic Hot Water Override Boost
Switches to PRT-HW model. Closes Terminals 3 & 4 (Hot Water Relay) and illuminates tap icon.
🧊
8. Sub-Zero Failsafe Pipe Frost Protection
Thermostat turned OFF in Standby, but room drops to 4°C. Emergency frost heat engages to save plumbing.

7. Interactive Bytecode Frame Inspector

Use the tool below to generate, inspect, and verify exact RS485 bytecode packets according to Section 3.1:

Generated Master Bytecode Frame (Hex & Dec)

Explanation will appear here.

8. Protocol Gotchas & Clarifications

The official Heatmiser specification contains subtleties that frequently cause integration bugs. Here are critical clarifications:

1. The Endianness Trap: Big-Endian Reads vs. Little-Endian Writes!

Page 4 states: "For those with 2 bytes parameters, when reading, high 8 bits are sent first while for writing the low 8 bit should be sent first".
For Holiday Hours (Address 24) and Countdown Minutes (Address 26):
• When reading from DCB: High byte is at index N, low byte is at index N+1 (Big-Endian).
• When writing command frames: Low byte is sent first, followed by high byte (Little-Endian).

2. Unique Address vs. DCB Index

In the PDF tables, there are two address columns:
• Unique Address: The network address used in write command frames (Bytes 4–5).
• Index in DCB: The zero-indexed byte position in the received read reply payload. These often differ! For instance, on TM1, Program Mode is Unique Address 16, but lives at DCB Index 6.

3. Hour 24 Represents a Cancelled Period

In both heating comfort levels and timer switching periods, an Hour value of 24 (0x18) denotes that this switching interval is cancelled/disabled, not midnight (which is Hour 00).

4. Temperature Sensor Scaling (Divided by 10)

Remote air temperature, floor temperature, and built-in temperature values are returned as 16-bit integers multiplied by 10. A value of 215 represents 21.5°C. If a sensor is disconnected or absent, the value returns 0xFFFF (65535).

9. Python API Specification & Developer Guide

The heatmiserv3 package provides an object-oriented Python interface adhering strictly to the Heatmiser V3 RS485 Protocol 3.9 specification. All convenience methods generate validated CCITT-16 checksummed frames, enforce hardware boundaries, and handle bi-directional telemetry conversion.

class heatmiserv3.connection.HeatmiserUH1(ipaddress=None, port=None, dev=None)
Hardware Interface

Connection manager and serial transport bridge for Heatmiser UH1 wiring centres and RS485 bus nodes. Supports direct serial ports (e.g. /dev/ttyUSB0, COM3) or TCP-to-serial network bridges (e.g. Moxa, USR-TCP232). Manages connection retry and automatic socket re-opening on bus disruptions.

  • ipaddress (str, optional) – IP address of TCP-to-RS485 bridge.
  • port (str | int, optional) – TCP port number of the bridge (e.g. "102" or 8000).
  • dev (str, optional) – Local POSIX serial device path or Windows COM port name.
Methods
def registerThermostat(thermostat: HeatmiserThermostat) → serial.Serial
Registers a thermostat instance with the UH1 connection and links its serial handle. Returns the active serial interface.
def reopen(force: bool = False) → bool | None
Closes the current socket or serial port and establishes a fresh connection, automatically updating the connection handle across all registered thermostat instances.
class heatmiserv3.heatmiser.HeatmiserThermostat(address, model, uh1)
Base Model

Primary model class representing Heatmiser digital heating thermostats: DT, DT-E, PRT, and PRT-E. Automatically retrieves and parses the initial DCB telemetry on initialization.

📘 Consistent Return Conventions
All parameter setter helpers validate boundaries and return True upon successful write dispatch, or False if an argument is invalid or out of specification range.
1. Device State & Lock Controls
def set_on_off(state: int | bool) → bool Write Addr 21
Sets the master power run state. 0 or False puts unit in Off / Frost Setback state. 1 or True restores normal heating operation.
def get_on_off() → int DCB Index 21
Returns the current run state: 0 (Off / Frost setback) or 1 (On / Normal).
def set_key_lock(lock: int | bool) → bool Write Addr 22
Locks (1 / True) or unlocks (0 / False) front-panel physical buttons to prevent manual tampering.
def get_key_lock() → int DCB Index 22
Returns keypad lock state: 0 (Unlocked) or 1 (Locked).
2. Temperature Regulation & Differential
def set_target_temp(temperature: int) → bool Write Addr 18
Sets target room comfort setpoint. Allowed range: 5°C to 35°C.
def get_target_temp() → int DCB Index 18
Returns current programmed target comfort temperature in °C.
def set_floormax_temp(floor_max: int) → bool Write Addr 19
Sets maximum floor safety cutout limit for underfloor heating. Allowed range: 20°C to 45°C.
def set_frost_protect_temp(frost_temp: int) → bool Write Addr 17
Sets frost protection setback temperature active when unit is switched off. Allowed range: 7°C to 17°C.
def set_temperature_format(temp_format: int | str) → bool Write Addr 5
Configures display unit. Accepts 0, "C", "Celsius" for °C; or 1, "F", "Fahrenheit" for °F.
def set_differential(diff: int) → bool Write Addr 6
Sets switching differential (hysteresis span) before relay fires. Allowed range: 1°C to 3°C.
3. Sensor Selection & Telemetry
def set_sensor_selection(sensor_mode: int | str) → bool Write Addr 13
Configures active temperature sensing source. Accepts integer codes 0..4 or descriptive strings:
  • 0 or "built in": Built-in air sensor
  • 1 or "remote air": Remote air sensor probe
  • 2 or "floor": Floor sensor probe only
  • 3 or "built-in+floor": Built-in air with floor overheat protection limit
  • 4 or "remote+floor": Remote air with floor overheat protection limit
def get_sensor_selection() → str DCB Index 13
Returns human-readable sensor mode string (e.g. "Built in air sensor", "Remote + floor").
4. Hold, Vacation & Real-Time Clock
def set_holiday_hours(hours: int) → bool Write Addr 24
Configures holiday setback duration in hours (range: 0..65535). Automatically formats as 2-byte little-endian payload on write. Writing 0 cancels holiday mode.
def set_temp_hold(minutes: int, temperature: int = None) → bool Write Addr 26
Places unit into temporary comfort hold for 1..1800 minutes. If temperature is specified (5–35°C), simultaneously writes the temporary comfort setpoint. Writing 0 minutes cancels active hold.
def set_time(week: int, hour: int, minute: int, second: int) → bool Write Addr 43
Sets hardware Real-Time Clock (RTC). week is 1–7 (1=Monday .. 7=Sunday). Hours 0–23, Minutes 0–59, Seconds 0–59.
def get_time() → dict DCB Indexes 36..39
Returns RTC dictionary: {"week": int, "hour": int, "min": int, "sec": int}.
class heatmiserv3.heatmiser.PRTHWThermostat(address, uh1, model="prt-hw")
Subclass of HeatmiserThermostat

Specialized class for the PRT-HW (Programmable Thermostat with Hot Water control, Model ID 0x04). Adds dedicated hot water boost management and enforces hardware constraints.

⚠️ Hardware Restriction: Sensor Selection
On PRT-HW hardware, external terminal blocks are assigned to the secondary Hot Water relay. Consequently, PRT-HW only supports Sensor Mode 0 (Built-in Air). Calling set_sensor_selection() with external or floor modes is safely refused and returns False.
Hot Water Methods
def set_hot_water_boost(state: int | bool) → bool Write Addr 41
Controls hot water boost channel. 1 or True engages Boost On immediately. 0 or False returns channel to programmed schedule (Auto).
def get_hot_water_state() → int DCB Index 36
Returns active hot water state: 0 (Off) or 1 (On).
def get_heating_state() → int DCB Index 35
Returns active space heating state: 0 (Off) or 1 (Heating active).
class heatmiserv3.heatmiser.TM1Thermostat(address, uh1, model="tm1")
Subclass of HeatmiserThermostat

Model class for the TM1 Time Clock & Countdown Programmer (Model ID 0x05). Provides timer channel overrides, countdown timing (0–1800m), and 5/2 & 7-day multi-period switching schedules.

Timer & Schedule Methods
def set_timer_state(state: int) → list Write Addr 42
Sets manual timer override: 0 = Auto (follows schedule), 1 = Override ON, 2 = Override OFF.
def set_countdown_timer(minutes: int) → list Write Addr 26
Sets countdown timer duration in minutes (0–1800). Automatically converts to 2-byte little-endian format.
def set_weekday_schedule(schedule: list[int]) → list Write Addr 71
Writes 16-byte weekday switching schedule representing 4 periods: [T1_On_H, T1_On_M, T1_Off_H, T1_Off_M, ...]. Cancelled periods are designated by setting Hour to 24.

Python Automation Examples

Example 1: PRT Room & Floor Temperature AutomationPython
from heatmiserv3 import heatmiser, connection

# Connect via serial bridge or direct RS485 adapter
uh1 = connection.HeatmiserUH1(ipaddress="192.168.1.57", port="102")

# Instantiate PRT programmable thermostat at address 1
prt = heatmiser.HeatmiserThermostat(address=1, model="prt", uh1=uh1)

# Configure device operating mode & regulation
prt.set_on_off(1)                       # Turn heating system ON
prt.set_temperature_format("C")         # Use Celsius
prt.set_differential(1)                 # 1°C switching differential
prt.set_target_temp(21)                 # Target comfort: 21°C
prt.set_floormax_temp(28)               # Floor safety cutout: 28°C

# Switch to Remote Air + Floor sensing mode (Mode 4)
prt.set_sensor_selection(4)

# Set 3-hour temporary comfort boost to 23°C
prt.set_temp_hold(minutes=180, temperature=23)
Example 2: PRT-HW Dual Channel Heating & Hot Water BoostPython
from heatmiserv3 import heatmiser, connection

uh1 = connection.HeatmiserUH1(dev="/dev/ttyUSB0")

# Instantiate PRT-HW thermostat at address 2
prt_hw = heatmiser.PRTHWThermostat(address=2, uh1=uh1)

# Read dual channel state
print("Heating Active:", prt_hw.get_heating_state())     # 1 or 0
print("Hot Water Active:", prt_hw.get_hot_water_state()) # 1 or 0

# Engage hot water boost
prt_hw.set_hot_water_boost(1)

# Reset boost back to scheduled auto mode
prt_hw.set_hot_water_boost(0)
Example 3: TM1 Time Clock & Countdown AutomationPython
from heatmiserv3 import heatmiser, connection

uh1 = connection.HeatmiserUH1(ipaddress="192.168.1.57", port="102")
tm1 = heatmiser.TM1Thermostat(address=3, uh1=uh1)

# 2-hour countdown timer
tm1.set_countdown_timer(120)

# Set 7-day vacation setback (168 hours)
tm1.set_holiday_hours(168)

# Configure 5/2 Weekday switching schedule (4 pairs: On H/M, Off H/M)
weekday_schedule = [
    6, 30,  8, 30,   # Time 1: 06:30 On, 08:30 Off
    17, 0, 22, 30,   # Time 2: 17:00 On, 22:30 Off
    24, 0, 24,  0,   # Time 3: Cancelled (Hour 24)
    24, 0, 24,  0,   # Time 4: Cancelled (Hour 24)
]
tm1.set_weekday_schedule(weekday_schedule)