Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
proto_heating.h
Go to the documentation of this file.
1#pragma once
2
3/// @file proto_heating.h
4/// @brief Pure codec for the private register protocol of 2W heaters (CMD_WRITE_PRIVATE 0x20).
5/// @ingroup hioc_protocol
6///
7/// A heater with an adjustable setpoint (node type 52, subtype 1) exposes its state as 16-bit
8/// registers `0x01xx`, read with `0C 60 01 <reg>` and written with `0C 61 01 <reg> <value>`; the
9/// device answers with CMD_WRITE_PRIVATE_ACK (0x21) echoing the header, followed by the value. Values
10/// are little-endian. An Atlantic AGILIA IO radiator answers every read and write this way
11/// (tests/corpus/captures/exchange/atlantic_agilia_exchange_{status_read,setpoint_and_off_read_back,mode_read_back}.yaml,
12/// pinned by corpus_heating_test.cpp); see also
13/// tests/corpus/captures/exchange/atlantic_thermor_exchange_write_private_param.yaml.
14///
15/// One descriptor table (`HEATING_FUNCTIONS` in the .cpp) lists every register this component
16/// uses: its access (read-only, read/write, write-only) and how its value is encoded. The same row
17/// drives the get payload, the set payload and the decoder of the reply, so a register is described
18/// exactly once. This module is pure: it does not touch a device record or the radio —
19/// proto_commands.h's create_write_private() frames a payload.
20///
21/// Confirmed on that radiator: the setpoint, mode (all four writable values), room temperature,
22/// regulator and maximum-setpoint registers. The presence register has not been written on hardware,
23/// and the window and open-window-detection registers have only been read at rest.
24
25#include "proto_frame.h"
26
27#include <cmath>
28#include <cstddef>
29#include <cstdint>
30
31namespace esphome {
32namespace home_io_control {
33
34/// @brief Registers and functions of a 2W heater, one row of the descriptor table each.
35///
36/// The `SET_*` names are the functions of the `heating_control` action; the same row is also the
37/// register's read path (reading SET_TEMPERATURE reads the comfort setpoint).
38enum class HeatingFunction : uint8_t {
39 POWER_ON, ///< Register 0x012C, read: the paired-device list. Logged, never decoded.
40 SET_TEMPERATURE, ///< Register 0x0103, read/write: comfort setpoint, tenths of a degree.
41 SET_MODE, ///< Register 0x0100, read/write: operating mode (HeatingMode).
42 SET_PRESENCE, ///< Register 0x0110, write-only: someone is in the room (1) or not (0).
43 SET_OPEN_WINDOW_DETECTION, ///< Register 0x0106, read/write: open-window detection on (1) or off (0).
44 MIDNIGHT_SYNC, ///< Register 0x0130, read: the comfort/eco/auto setpoint block. Logged, never decoded.
45 ROOM_TEMPERATURE, ///< Register 0x0115, read-only: measured room temperature, signed hundredths of a degree.
46 REGULATOR_STATE, ///< Register 0x0117, read-only: 0 none, 1 standby, 2 heating.
47 MAX_SETPOINT, ///< Register 0x0102, read-only: highest setpoint the heater accepts, tenths of a degree.
48 WINDOW_OPEN, ///< Register 0x010E, read-only: the heater's window sensor, 0 closed / 1 open.
49};
50
51/// @brief Operating modes of the mode register (HeatingFunction::SET_MODE).
52enum class HeatingMode : uint8_t {
53 AUTO = 0x00, ///< Automatic mode; the device manages the setpoint itself.
54 MANUAL = 0x01, ///< Manual mode; follows the last written setpoint.
55 PROG = 0x02, ///< Program mode; the device runs its own stored weekly schedule.
56 EXTERNAL_SETPOINT = 0x03, ///< Setpoint comes from an external controller. Readable, never written by this
57 ///< component. Not named EXTERNAL: the Arduino core defines that as a macro.
58 OFF = 0x04, ///< Off / standby.
59};
60
61/// @brief Regulator states of the regulator register (HeatingFunction::REGULATOR_STATE).
62enum class HeatingRegulatorState : uint8_t {
63 NONE = 0x00, ///< No regulation active.
64 STANDBY = 0x01, ///< Regulating, the element is idle.
65 HEATING = 0x02, ///< The element is heating.
66};
67
68/// @brief Lowest setpoint this codec will encode.
69///
70/// The conventional frost-protection setpoint, safely representable as wire value 70.
71constexpr float HEATING_TEMP_MIN_C = 7.0F;
72
73/// @brief Highest setpoint this codec will encode.
74///
75/// The setpoint register is a 16-bit little-endian value in tenths of a degree; 28.0 °C (`18 01`) is
76/// the ceiling Atlantic radiator manuals document and the value an AGILIA IO radiator reports in its
77/// maximum setpoint register (tests/corpus/captures/exchange/atlantic_agilia_exchange_status_read.yaml).
78/// No setpoint above 25.5 °C (a non-zero high byte) has been written on hardware yet.
79constexpr float HEATING_TEMP_MAX_C = 28.0F;
80
81/// @brief Largest payload any function produces: a set of a 16-bit value (4-byte header + 2 value bytes).
82constexpr size_t HEATING_PAYLOAD_MAX_SIZE = 6;
83
84/// @brief Leading payload byte, common to every function. Its meaning is unconfirmed; do not read a
85/// semantic (for example "device class") into it.
86constexpr uint8_t HEATING_PAYLOAD_PREFIX = 0x0C;
87
88/// @brief Payload byte at index 2: the high byte of the 16-bit register number, constant `0x01`.
89constexpr uint8_t HEATING_REGISTER_HIGH_BYTE = 0x01;
90
91/// @brief Payload byte at index 1 of a read ("get") request.
92constexpr uint8_t HEATING_DIRECTION_GET = 0x60;
93
94/// @brief Payload byte at index 1 of a write ("set") request.
95constexpr uint8_t HEATING_DIRECTION_SET = 0x61;
96
97/// @brief Measured value in a reading when the heater reports a sensor fault (raw `-1`).
98/// A reading stores it as NaN, the same as "never read".
99constexpr int16_t HEATING_SENSOR_FAULT_RAW = -1;
100
101/// @brief Default encoding range of a temperature main parameter, in degrees Celsius. The main
102/// parameter spans HEATING_TEMPERATURE_MP_SPAN steps over [min, max]; this range is the one a device
103/// is assumed to use unless the YAML says otherwise (`min_temperature:` / `max_temperature:`).
104static constexpr float HEATING_TEMPERATURE_RANGE_MIN_C = -40.0F;
105static constexpr float HEATING_TEMPERATURE_RANGE_MAX_C = 80.0F; ///< See HEATING_TEMPERATURE_RANGE_MIN_C.
106
107/// @brief Stable lowercase name for a heating function ("power_on", "set_temperature", ...).
108/// @param fn Function.
109/// @return Null-terminated string; "unknown" for an out-of-range value.
111
112/// @brief Whether the register behind a function can be written.
113/// @param fn Function.
114/// @return true for a read/write or write-only register; false for a read-only one or an
115/// out-of-range value.
117
118/// @brief Whether the register behind a function can be read.
119/// @param fn Function.
120/// @return true for a read/write or read-only register; false for a write-only one or an
121/// out-of-range value.
123
124/// @brief Encode a write ("set") of one register into a CMD_WRITE_PRIVATE (0x20) payload.
125///
126/// `value` is interpreted by the register's encoding:
127/// - SET_TEMPERATURE: degrees Celsius within [HEATING_TEMP_MIN_C, HEATING_TEMP_MAX_C], encoded as
128/// a 16-bit little-endian value in tenths of a degree, round-half-away-from-zero(10 * value).
129/// - SET_MODE: a HeatingMode value widened to float; must be AUTO, MANUAL, PROG or OFF.
130/// - SET_PRESENCE, SET_OPEN_WINDOW_DETECTION: 0 or 1.
131/// A read-only register (including POWER_ON, MIDNIGHT_SYNC and the measurement registers) is never
132/// encoded as a write, and out-of-range, non-integral (for enum/binary encodings) or non-finite
133/// input is rejected: a byte derived from a truncating or wrapping cast is never emitted.
134/// @param fn Register to write.
135/// @param value Register-specific value (see above).
136/// @param out Output buffer of HEATING_PAYLOAD_MAX_SIZE bytes.
137/// @return Number of payload bytes written (5 or 6), or 0 on invalid input or a read-only register.
138size_t encode_heating_write(HeatingFunction fn, float value, uint8_t out[HEATING_PAYLOAD_MAX_SIZE]);
139
140/// @brief Encode a read ("get") of one register into a CMD_WRITE_PRIVATE (0x20) payload.
141/// @param fn Register to read.
142/// @param out Output buffer of HEATING_PAYLOAD_MAX_SIZE bytes.
143/// @return 4 (the payload is always `0C 60 01 <reg>`), or 0 for a write-only register or an
144/// out-of-range value.
146
147/// @brief Encode the function the `heating_control` action runs: a read for a read-only register,
148/// a write for every other.
149///
150/// POWER_ON and MIDNIGHT_SYNC are reads (their `value` is ignored); the measurement registers are
151/// reads too; the others are writes as in encode_heating_write().
152/// @param fn Function to encode.
153/// @param value Function-specific value (ignored for a read).
154/// @param out Output buffer of HEATING_PAYLOAD_MAX_SIZE bytes.
155/// @return Number of payload bytes written (4, 5 or 6), or 0 on invalid input.
156size_t encode_heating_payload(HeatingFunction fn, float value, uint8_t out[HEATING_PAYLOAD_MAX_SIZE]);
157
158/// @brief Decode the value a heater answered a register read or write with.
159///
160/// Accepts a CMD_WRITE_PRIVATE_ACK (0x21) whose data starts with `0C 60 01 <reg>` or
161/// `0C 61 01 <reg>` for this function's register, followed by at least the register's value bytes.
162/// Anything else (another command, another register, a truncated value, a function without a typed
163/// decode such as POWER_ON) is "no value".
164///
165/// The value is returned in natural units: degrees Celsius for the temperature registers, the raw
166/// byte for the others. A raw `-1` on the room-temperature register is a sensor fault: it decodes
167/// as NaN with `true` returned, because the heater did answer.
168/// @param reply The device's reply frame.
169/// @param fn Register the request addressed.
170/// @param[out] value Decoded value; untouched when `false` is returned.
171/// @return true when the reply carries a value for this register.
172bool decode_heating_reply(const IoFrame &reply, HeatingFunction fn, float &value);
173
174/// @brief What the hub has read from one heater so far.
175///
176/// Every field starts as "never read" (NaN, or -1 for the byte registers). Only a decoded reply
177/// changes a field, so what an entity shows from here is observed, never predicted.
179 float room_temperature_c{NAN}; ///< Measured room temperature; NaN until read or on a sensor fault.
180 float setpoint_c{NAN}; ///< Comfort setpoint as the heater holds it; NaN until read.
181 float max_setpoint_c{NAN}; ///< Highest setpoint the heater accepts; NaN until read.
182 int16_t mode{-1}; ///< HeatingMode value; -1 until read.
183 int16_t regulator{-1}; ///< HeatingRegulatorState value; -1 until read.
184 int16_t window_open{-1}; ///< 0 closed / 1 open; -1 until read.
185 int16_t open_window_detection{-1}; ///< 1 detection on / 0 off; -1 until read.
186 int16_t level{-1}; ///< Low byte of the main parameter of a heater commanded with a prefixed
187 ///< value: a heating level, or a setpoint alias. -1 until a status reply
188 ///< carried one, and again after a reply that carried a plain
189 ///< temperature. Set by the hub's status path, not by store().
190 int32_t main_parameter{-1}; ///< The raw current main parameter of a heater commanded with an EXECUTE
191 ///< (a temperature, an alias or a level); -1 until a status reply
192 ///< carried one. Set by the hub's status path, not by store().
193
194 /// @brief Fold one decoded register value into the readings.
195 /// @param fn Register the value was read from.
196 /// @param value Value as returned by decode_heating_reply().
197 /// @return true when `fn` is a register the readings hold (and the field was set).
198 bool store(HeatingFunction fn, float value);
199
200 /// @brief Has this register been read yet?
201 /// @param fn Register.
202 /// @return true when the readings hold a value for `fn`; false for "never read" and for a function
203 /// the readings do not hold.
204 [[nodiscard]] bool has(HeatingFunction fn) const;
205};
206
207} // namespace home_io_control
208} // namespace esphome
bool decode_heating_reply(const IoFrame &reply, HeatingFunction fn, float &value)
Decode the value a heater answered a register read or write with.
constexpr size_t HEATING_PAYLOAD_MAX_SIZE
Largest payload any function produces: a set of a 16-bit value (4-byte header + 2 value bytes).
HeatingRegulatorState
Regulator states of the regulator register (HeatingFunction::REGULATOR_STATE).
@ STANDBY
Regulating, the element is idle.
size_t encode_heating_payload(HeatingFunction fn, float value, uint8_t out[HEATING_PAYLOAD_MAX_SIZE])
Encode the function the heating_control action runs: a read for a read-only register,...
HeatingFunction
Registers and functions of a 2W heater, one row of the descriptor table each.
@ MIDNIGHT_SYNC
Register 0x0130, read: the comfort/eco/auto setpoint block. Logged, never decoded.
@ ROOM_TEMPERATURE
Register 0x0115, read-only: measured room temperature, signed hundredths of a degree.
@ MAX_SETPOINT
Register 0x0102, read-only: highest setpoint the heater accepts, tenths of a degree.
@ POWER_ON
Register 0x012C, read: the paired-device list. Logged, never decoded.
@ SET_PRESENCE
Register 0x0110, write-only: someone is in the room (1) or not (0).
@ REGULATOR_STATE
Register 0x0117, read-only: 0 none, 1 standby, 2 heating.
@ WINDOW_OPEN
Register 0x010E, read-only: the heater's window sensor, 0 closed / 1 open.
@ SET_OPEN_WINDOW_DETECTION
Register 0x0106, read/write: open-window detection on (1) or off (0).
@ SET_TEMPERATURE
Register 0x0103, read/write: comfort setpoint, tenths of a degree.
@ SET_MODE
Register 0x0100, read/write: operating mode (HeatingMode).
constexpr uint8_t HEATING_REGISTER_HIGH_BYTE
Payload byte at index 2: the high byte of the 16-bit register number, constant 0x01.
bool heating_function_is_readable(HeatingFunction fn)
Whether the register behind a function can be read.
constexpr float HEATING_TEMP_MAX_C
Highest setpoint this codec will encode.
static constexpr float HEATING_TEMPERATURE_RANGE_MAX_C
See HEATING_TEMPERATURE_RANGE_MIN_C.
const char * heating_function_name(HeatingFunction fn)
Stable lowercase name for a heating function ("power_on", "set_temperature", ...).
constexpr uint8_t HEATING_DIRECTION_SET
Payload byte at index 1 of a write ("set") request.
constexpr float HEATING_TEMP_MIN_C
Lowest setpoint this codec will encode.
size_t encode_heating_write(HeatingFunction fn, float value, uint8_t out[HEATING_PAYLOAD_MAX_SIZE])
Encode a write ("set") of one register into a CMD_WRITE_PRIVATE (0x20) payload.
static constexpr float HEATING_TEMPERATURE_RANGE_MIN_C
Default encoding range of a temperature main parameter, in degrees Celsius.
HeatingMode
Operating modes of the mode register (HeatingFunction::SET_MODE).
@ PROG
Program mode; the device runs its own stored weekly schedule.
@ EXTERNAL_SETPOINT
Setpoint comes from an external controller.
@ MANUAL
Manual mode; follows the last written setpoint.
@ AUTO
Automatic mode; the device manages the setpoint itself.
constexpr int16_t HEATING_SENSOR_FAULT_RAW
Measured value in a reading when the heater reports a sensor fault (raw -1).
constexpr uint8_t HEATING_PAYLOAD_PREFIX
Leading payload byte, common to every function.
bool heating_function_is_writable(HeatingFunction fn)
Whether the register behind a function can be written.
size_t encode_heating_read(HeatingFunction fn, uint8_t out[HEATING_PAYLOAD_MAX_SIZE])
Encode a read ("get") of one register into a CMD_WRITE_PRIVATE (0x20) payload.
constexpr uint8_t HEATING_DIRECTION_GET
Payload byte at index 1 of a read ("get") request.
IO-Homecontrol 2W frame container: control bytes, IoFrame and (de)serialization.
What the hub has read from one heater so far.
int16_t window_open
0 closed / 1 open; -1 until read.
int16_t level
Low byte of the main parameter of a heater commanded with a prefixed value: a heating level,...
int16_t open_window_detection
1 detection on / 0 off; -1 until read.
bool has(HeatingFunction fn) const
Has this register been read yet?
float setpoint_c
Comfort setpoint as the heater holds it; NaN until read.
float room_temperature_c
Measured room temperature; NaN until read or on a sensor fault.
int32_t main_parameter
The raw current main parameter of a heater commanded with an EXECUTE (a temperature,...
float max_setpoint_c
Highest setpoint the heater accepts; NaN until read.
int16_t mode
HeatingMode value; -1 until read.
int16_t regulator
HeatingRegulatorState value; -1 until read.
bool store(HeatingFunction fn, float value)
Fold one decoded register value into the readings.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
Definition proto_frame.h:93