Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
proto_heating.cpp
Go to the documentation of this file.
1/// @file proto_heating.cpp
2/// @brief Pure codec for the private register protocol of 2W heaters (CMD_WRITE_PRIVATE 0x20).
3/// @ingroup hioc_protocol
4
5#include "proto_heating.h"
6
7#include "proto_constants.h"
8#include "proto_sizes.h"
9
10namespace esphome {
11namespace home_io_control {
12
13namespace {
14
15/// Mask for the low byte of a 16-bit little-endian register value.
16constexpr uint16_t LOW_BYTE_MASK = 0xFF;
17
18/// Payload length of a read, and of a write that carries no value ({prefix, direction, reg-high, reg-low}).
19constexpr size_t PAYLOAD_LEN_HEADER = 4;
20/// Payload length for a write that carries one value byte (mode / presence / window detection).
21constexpr size_t PAYLOAD_LEN_ONE_VALUE = 5;
22/// Payload length for a write of a 16-bit value (the setpoint).
23constexpr size_t PAYLOAD_LEN_TWO_VALUE = HEATING_PAYLOAD_MAX_SIZE;
24
25/// Wire scale of the setpoint registers: round(degrees * this), little-endian 16-bit.
26constexpr float DEGREES_TO_TENTHS = 10.0F;
27/// Scale of the room-temperature register: signed hundredths of a degree.
28constexpr float HUNDREDTHS_PER_DEGREE = 100.0F;
29
30/// Highest regulator-state value the register defines (HeatingRegulatorState::HEATING).
31constexpr uint8_t REGULATOR_STATE_MAX = static_cast<uint8_t>(HeatingRegulatorState::HEATING);
32/// Highest towel-dryer mode value the register defines (TowelDryerMode::MANUAL).
33constexpr uint8_t TOWEL_MODE_MAX = static_cast<uint8_t>(TowelDryerMode::MANUAL);
34/// Highest temporary-state value (HeatingTemporaryState::DRYING).
35constexpr uint8_t TEMPORARY_STATE_MAX = static_cast<uint8_t>(HeatingTemporaryState::DRYING);
36/// Highest working rate: a percentage.
37constexpr uint8_t WORKING_RATE_MAX = 100;
38
39/// @brief How a register's value maps onto the bytes after the 4-byte header.
40enum class HeatingValueKind : uint8_t {
41 NONE, ///< No typed value: logged raw, never decoded.
42 TENTHS_U16, ///< Unsigned 16-bit little-endian tenths of a degree.
43 HUNDREDTHS_S16, ///< Signed 16-bit little-endian hundredths of a degree; -1 is a sensor fault.
44 BINARY, ///< One byte, 0 or 1.
45 MODE, ///< One HeatingMode byte.
46 BOUNDED_BYTE, ///< One byte from 0 to the row's `max_value`: an enumeration (regulator state, towel-dryer
47 ///< mode, temporary state) or a percentage (working rate).
48 BOUNDED_U16, ///< Unsigned 16-bit little-endian integer from 0 to the row's `max_value` (night light brightness).
49};
50
51/// @brief Who may do what with a register.
52enum class HeatingAccess : uint8_t {
53 READ_ONLY, ///< Reads only; a write is refused by the codec.
54 READ_WRITE, ///< Reads and writes.
55 WRITE_ONLY, ///< Writes only; the heater has no readable value for it.
56};
57
58/// @brief One register of the heater, the single description both directions are built from.
59///
60/// A request is {HEATING_PAYLOAD_PREFIX, direction, HEATING_REGISTER_HIGH_BYTE, register_low,
61/// [value...]}; `direction` is HEATING_DIRECTION_GET or HEATING_DIRECTION_SET.
62struct HeatingFunctionDescriptor {
63 const char *name; ///< Action-visible name.
64 HeatingFunction fn; ///< Function this row describes.
65 uint8_t register_low; ///< Payload byte 3: low byte of the 0x01xx register number.
66 HeatingAccess access; ///< Whether the register may be read, written or both.
67 HeatingValueKind kind; ///< How the value maps onto the trailing bytes.
68 uint8_t max_value{0}; ///< BOUNDED_BYTE, BOUNDED_U16: highest value a reply may carry and a write may send.
69};
70
71constexpr HeatingFunctionDescriptor HEATING_FUNCTIONS[] = {
72 {"power_on", HeatingFunction::POWER_ON, 0x2C, HeatingAccess::READ_ONLY, HeatingValueKind::NONE},
73 {"set_temperature", HeatingFunction::SET_TEMPERATURE, 0x03, HeatingAccess::READ_WRITE,
74 HeatingValueKind::TENTHS_U16},
75 {"set_mode", HeatingFunction::SET_MODE, 0x00, HeatingAccess::READ_WRITE, HeatingValueKind::MODE},
76 {"set_presence", HeatingFunction::SET_PRESENCE, 0x10, HeatingAccess::WRITE_ONLY, HeatingValueKind::BINARY},
77 {"set_open_window_detection", HeatingFunction::SET_OPEN_WINDOW_DETECTION, 0x06, HeatingAccess::READ_WRITE,
78 HeatingValueKind::BINARY},
79 {"midnight_sync", HeatingFunction::MIDNIGHT_SYNC, 0x30, HeatingAccess::READ_ONLY, HeatingValueKind::NONE},
80 {"room_temperature", HeatingFunction::ROOM_TEMPERATURE, 0x15, HeatingAccess::READ_ONLY,
81 HeatingValueKind::HUNDREDTHS_S16},
82 {"regulator_state", HeatingFunction::REGULATOR_STATE, 0x17, HeatingAccess::READ_ONLY,
83 HeatingValueKind::BOUNDED_BYTE, REGULATOR_STATE_MAX},
84 {"max_setpoint", HeatingFunction::MAX_SETPOINT, 0x02, HeatingAccess::READ_ONLY, HeatingValueKind::TENTHS_U16},
85 {"window_open", HeatingFunction::WINDOW_OPEN, 0x0E, HeatingAccess::READ_ONLY, HeatingValueKind::BINARY},
86 // Towel-dryer registers: read on two Atlantic Adelis units (corpus atlantic_adelis_probe_*); writing
87 // 0x0136 (mode) and 0x013E (boost, drying) is confirmed against the display (issue #202).
88 {"towel_mode", HeatingFunction::TOWEL_MODE, 0x36, HeatingAccess::READ_WRITE, HeatingValueKind::BOUNDED_BYTE,
89 TOWEL_MODE_MAX},
90 {"temporary_state", HeatingFunction::TEMPORARY_STATE, 0x3E, HeatingAccess::READ_WRITE,
91 HeatingValueKind::BOUNDED_BYTE, TEMPORARY_STATE_MAX},
92 {"working_rate", HeatingFunction::WORKING_RATE, 0x26, HeatingAccess::READ_ONLY, HeatingValueKind::BOUNDED_BYTE,
93 WORKING_RATE_MAX},
94 // Read on a towel dryer in a programme period (issue #202): the temperature in force in 0x0137 and the
95 // eco setback in 0x0104.
96 {"temperature_in_force", HeatingFunction::TEMPERATURE_IN_FORCE, 0x37, HeatingAccess::READ_ONLY,
97 HeatingValueKind::TENTHS_U16},
98 {"eco_setback", HeatingFunction::ECO_SETBACK, 0x04, HeatingAccess::READ_ONLY, HeatingValueKind::TENTHS_U16},
99 // Night light registers: read on a Sauter Hekla with the light on and off (issue #222).
100 {"night_light", HeatingFunction::NIGHT_LIGHT, 0x44, HeatingAccess::READ_WRITE, HeatingValueKind::BINARY},
101 {"night_light_brightness", HeatingFunction::NIGHT_LIGHT_BRIGHTNESS, 0x42, HeatingAccess::READ_WRITE,
102 HeatingValueKind::BOUNDED_U16, HEATING_NIGHT_LIGHT_BRIGHTNESS_MAX},
103};
104
105/// @brief Look up the descriptor for a function.
106/// @param fn Function to resolve.
107/// @return Pointer into the constexpr table, or nullptr for an out-of-range value.
108const HeatingFunctionDescriptor *descriptor_for(HeatingFunction fn) {
109 for (const auto &d : HEATING_FUNCTIONS) {
110 if (d.fn == fn)
111 return &d;
112 }
113 return nullptr;
114}
115
116/// @brief Write the 4-byte request header for a row.
117void write_header(const HeatingFunctionDescriptor &d, uint8_t direction, uint8_t *out) {
118 out[0] = HEATING_PAYLOAD_PREFIX;
119 out[1] = direction;
121 out[3] = d.register_low;
122}
123
124/// @brief Number of value bytes behind the header for a kind.
125size_t value_length(HeatingValueKind kind) {
126 switch (kind) {
127 case HeatingValueKind::NONE:
128 return 0;
129 case HeatingValueKind::TENTHS_U16:
130 case HeatingValueKind::HUNDREDTHS_S16:
131 case HeatingValueKind::BOUNDED_U16:
132 return 2;
133 case HeatingValueKind::BINARY:
134 case HeatingValueKind::MODE:
135 case HeatingValueKind::BOUNDED_BYTE:
136 return 1;
137 }
138 return 0;
139}
140
141/// @brief Is `value` an integer from 0 to `max_value`? NaN fails every comparison, so it is refused too.
142bool is_bounded_integer(float value, uint8_t max_value) {
143 return std::isfinite(value) && value >= 0.0F && value <= static_cast<float>(max_value) && value == std::floor(value);
144}
145
146/// @brief Write a set request for a 16-bit register: the header, then the value low byte first, the
147/// order the register reads back in.
148/// @return PAYLOAD_LEN_TWO_VALUE.
149size_t write_u16_set(const HeatingFunctionDescriptor &d, uint16_t raw, uint8_t *out) {
150 write_header(d, HEATING_DIRECTION_SET, out);
151 out[4] = static_cast<uint8_t>(raw & LOW_BYTE_MASK);
152 out[PAYLOAD_LEN_TWO_VALUE - 1] = static_cast<uint8_t>(raw >> BITS_PER_BYTE);
153 return PAYLOAD_LEN_TWO_VALUE;
154}
155
156} // namespace
157
159 const HeatingFunctionDescriptor *d = descriptor_for(fn);
160 return d != nullptr ? d->name : "unknown";
161}
162
164 const HeatingFunctionDescriptor *d = descriptor_for(fn);
165 return d != nullptr && d->access != HeatingAccess::READ_ONLY;
166}
167
169 const HeatingFunctionDescriptor *d = descriptor_for(fn);
170 return d != nullptr && d->access != HeatingAccess::WRITE_ONLY;
171}
172
173bool heating_function_register(HeatingFunction fn, uint8_t &register_low) {
174 const HeatingFunctionDescriptor *d = descriptor_for(fn);
175 if (d == nullptr)
176 return false;
177 register_low = d->register_low;
178 return true;
179}
180
181bool heating_reply_echoes_register(const IoFrame &reply, uint8_t reg_hi, uint8_t reg_lo) {
182 if (reply.cmd != CMD_WRITE_PRIVATE_ACK || reply.data_len < HEATING_REPLY_VALUE_OFFSET)
183 return false;
184 const uint8_t *data = reply.data;
185 if (data[0] != HEATING_PAYLOAD_PREFIX || data[2] != reg_hi || data[3] != reg_lo)
186 return false;
187 return data[1] == HEATING_DIRECTION_GET || data[1] == HEATING_DIRECTION_SET;
188}
189
191 const HeatingFunctionDescriptor *d = descriptor_for(fn);
192 if (d == nullptr || d->access == HeatingAccess::WRITE_ONLY)
193 return 0;
194 write_header(*d, HEATING_DIRECTION_GET, out);
195 return PAYLOAD_LEN_HEADER;
196}
197
198size_t encode_heating_register_read(uint8_t reg_hi, uint8_t reg_lo, uint8_t out[HEATING_PAYLOAD_MAX_SIZE]) {
199 // The direction is the GET constant and nothing else: this is the one frame builder the register
200 // probes use, so it is where "read-only" is enforced.
201 out[0] = HEATING_PAYLOAD_PREFIX;
202 out[1] = HEATING_DIRECTION_GET;
203 out[2] = reg_hi;
204 out[3] = reg_lo;
205 return PAYLOAD_LEN_HEADER;
206}
207
208size_t encode_heating_write(HeatingFunction fn, float value, uint8_t out[HEATING_PAYLOAD_MAX_SIZE]) {
209 const HeatingFunctionDescriptor *d = descriptor_for(fn);
210 // A read-only register is never written, whatever the caller asks: the heater's window sensor and
211 // its measurements are not settings.
212 if (d == nullptr || d->access == HeatingAccess::READ_ONLY)
213 return 0;
214
215 switch (d->kind) {
216 case HeatingValueKind::TENTHS_U16: {
217 // Reject NaN / infinity and anything outside the representable range rather than emitting a
218 // wrapped byte.
219 if (!std::isfinite(value) || value < HEATING_TEMP_MIN_C || value > HEATING_TEMP_MAX_C)
220 return 0;
221 // Multiply in float (not double): the float32 product of e.g. 20.55f * 10.0f is exactly
222 // 205.5f, and std::round() takes that half away from zero to 206 (0x00CE). Truncating or
223 // rounding toward even would both be wrong here.
224 // NB: this exactness relies on FLT_EVAL_METHOD == 0 (operands evaluated at their own single
225 // precision, no hidden widening to double). That holds on x86-64 SSE and on Xtensa; the
226 // proto_heating test pins 20.55 C, which is one ULP from flipping to 0xCD under any
227 // excess-precision evaluation.
228 return write_u16_set(*d, static_cast<uint16_t>(std::round(value * DEGREES_TO_TENTHS)), out);
229 }
230
231 case HeatingValueKind::MODE:
232 // Exact float match against the four modes this component writes; a non-integral or unknown
233 // value (the "external" mode included) is rejected, not coerced.
234 if (value != static_cast<float>(HeatingMode::AUTO) && value != static_cast<float>(HeatingMode::MANUAL) &&
235 value != static_cast<float>(HeatingMode::PROG) && value != static_cast<float>(HeatingMode::OFF))
236 return 0;
237 write_header(*d, HEATING_DIRECTION_SET, out);
238 out[4] = static_cast<uint8_t>(value);
239 return PAYLOAD_LEN_ONE_VALUE;
240
241 case HeatingValueKind::BINARY:
242 if (value != 0.0F && value != 1.0F)
243 return 0;
244 write_header(*d, HEATING_DIRECTION_SET, out);
245 out[4] = static_cast<uint8_t>(value);
246 return PAYLOAD_LEN_ONE_VALUE;
247
248 case HeatingValueKind::BOUNDED_BYTE:
249 // An integral value inside the register's range; anything else is refused, never truncated.
250 if (!is_bounded_integer(value, d->max_value))
251 return 0;
252 write_header(*d, HEATING_DIRECTION_SET, out);
253 out[4] = static_cast<uint8_t>(value);
254 return PAYLOAD_LEN_ONE_VALUE;
255
256 case HeatingValueKind::BOUNDED_U16:
257 // The same rule, in the two-byte form.
258 if (!is_bounded_integer(value, d->max_value))
259 return 0;
260 return write_u16_set(*d, static_cast<uint16_t>(value), out);
261
262 case HeatingValueKind::NONE:
263 case HeatingValueKind::HUNDREDTHS_S16:
264 return 0; // no writable register has this encoding
265 }
266 return 0;
267}
268
270 const HeatingFunctionDescriptor *d = descriptor_for(fn);
271 if (d == nullptr)
272 return 0;
273 return d->access == HeatingAccess::READ_ONLY ? encode_heating_read(fn, out) : encode_heating_write(fn, value, out);
274}
275
276bool decode_heating_reply(const IoFrame &reply, HeatingFunction fn, float &value) {
277 const HeatingFunctionDescriptor *d = descriptor_for(fn);
278 if (d == nullptr || d->kind == HeatingValueKind::NONE ||
280 return false;
281 if (reply.data_len < HEATING_REPLY_VALUE_OFFSET + value_length(d->kind))
282 return false;
283
284 const uint8_t *v = reply.data + HEATING_REPLY_VALUE_OFFSET;
285 const auto raw16 = static_cast<uint16_t>(v[0] | (v[1] << BITS_PER_BYTE));
286 switch (d->kind) {
287 case HeatingValueKind::TENTHS_U16:
288 value = static_cast<float>(raw16) / DEGREES_TO_TENTHS;
289 return true;
290
291 case HeatingValueKind::HUNDREDTHS_S16: {
292 const auto signed_raw = static_cast<int16_t>(raw16);
293 value = signed_raw == HEATING_SENSOR_FAULT_RAW ? NAN : static_cast<float>(signed_raw) / HUNDREDTHS_PER_DEGREE;
294 return true;
295 }
296
297 case HeatingValueKind::BINARY:
298 if (v[0] > 1)
299 return false;
300 value = static_cast<float>(v[0]);
301 return true;
302
303 case HeatingValueKind::MODE:
304 if (v[0] > static_cast<uint8_t>(HeatingMode::OFF))
305 return false;
306 value = static_cast<float>(v[0]);
307 return true;
308
309 case HeatingValueKind::BOUNDED_BYTE:
310 if (v[0] > d->max_value)
311 return false;
312 value = static_cast<float>(v[0]);
313 return true;
314
315 case HeatingValueKind::BOUNDED_U16:
316 if (raw16 > d->max_value)
317 return false;
318 value = static_cast<float>(raw16);
319 return true;
320
321 case HeatingValueKind::NONE:
322 return false;
323 }
324 return false;
325}
326
327// === HeatingReadings: one table from register to field ===
328
329namespace {
330
331/// @brief Which field of HeatingReadings holds a register, and how its value is kept.
332struct HeatingReadingField {
333 HeatingFunction fn; ///< Register.
334 float HeatingReadings::*temperature; ///< The field of a register read in degrees (NaN until read), else nullptr.
335 int16_t HeatingReadings::*
336 integer; ///< The field of a register read as a byte or a flag (-1 until read), else nullptr.
337};
338
339/// Every register the readings hold. POWER_ON, SET_PRESENCE and MIDNIGHT_SYNC have no row: they are logged or
340/// written, never kept.
341constexpr HeatingReadingField HEATING_READING_FIELDS[] = {
356};
357
358/// @return The row of a register, or nullptr for one the readings do not hold.
359const HeatingReadingField *reading_field_for(HeatingFunction fn) {
360 for (const auto &field : HEATING_READING_FIELDS) {
361 if (field.fn == fn)
362 return &field;
363 }
364 return nullptr;
365}
366
367} // namespace
368
370 const HeatingReadingField *field = reading_field_for(fn);
371 if (field == nullptr)
372 return false;
373 if (field->temperature != nullptr) {
374 this->*field->temperature = value;
375 } else {
376 this->*field->integer = static_cast<int16_t>(value);
377 }
378 return true;
379}
380
382 const HeatingReadingField *field = reading_field_for(fn);
383 if (field == nullptr)
384 return false;
385 if (field->temperature != nullptr) {
386 this->*field->temperature = NAN;
387 } else {
388 this->*field->integer = -1;
389 }
390 return true;
391}
392
394 const HeatingReadingField *field = reading_field_for(fn);
395 if (field == nullptr)
396 return false;
397 return field->temperature != nullptr ? !std::isnan(this->*field->temperature) : this->*field->integer >= 0;
398}
399
401 const HeatingReadingField *field = reading_field_for(fn);
402 return field != nullptr && field->temperature != nullptr ? this->*field->temperature : NAN;
403}
404
406 // Temperatures and the registers the readings do not hold have no wire byte.
407 const HeatingReadingField *field = reading_field_for(fn);
408 return field != nullptr && field->integer != nullptr ? this->*field->integer : -1;
409}
410
411} // namespace home_io_control
412} // namespace esphome
constexpr size_t HEATING_REPLY_VALUE_OFFSET
Offset of the first value byte in a register reply: the four header bytes it echoes.
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).
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.
@ TOWEL_MODE
Register 0x0136, read/write: a towel dryer's mode (TowelDryerMode).
@ ROOM_TEMPERATURE
Register 0x0115, read-only: measured room temperature, signed hundredths of a degree.
@ WORKING_RATE
Register 0x0126, read-only: share of the time the element heats, 0-100 %.
@ NIGHT_LIGHT
Register 0x0144, read/write: a heater's night light on (1) or off (0).
@ 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.
@ TEMPORARY_STATE
Register 0x013E, read/write: a towel dryer's temporary state (HeatingTemporaryState).
@ SET_PRESENCE
Register 0x0110, write-only: someone is in the room (1) or not (0).
@ NIGHT_LIGHT_BRIGHTNESS
Register 0x0142, read/write: the night light's brightness, 0-100 %, 16-bit.
@ 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.
@ TEMPERATURE_IN_FORCE
Register 0x0137, read-only: the temperature a towel dryer holds right now, tenths of a degree.
@ ECO_SETBACK
Register 0x0104, read-only: how far below the comfort setpoint a towel dryer's eco period heats,...
@ 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.
@ MANUAL
Manual mode: the device holds the comfort setpoint.
constexpr float HEATING_TEMP_MAX_C
Highest setpoint this codec will encode.
const char * heating_function_name(HeatingFunction fn)
Stable lowercase name for a heating function ("power_on", "set_temperature", ...).
bool heating_reply_echoes_register(const IoFrame &reply, uint8_t reg_hi, uint8_t reg_lo)
Is this frame a register reply that echoes a request for this register?
constexpr uint8_t HEATING_DIRECTION_SET
Payload byte at index 1 of a write ("set") request.
constexpr uint8_t HEATING_NIGHT_LIGHT_BRIGHTNESS_MAX
Highest night light brightness (HeatingFunction::NIGHT_LIGHT_BRIGHTNESS): a percentage,...
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.
@ PROG
Program mode; the device runs its own stored weekly schedule.
@ 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.
size_t encode_heating_register_read(uint8_t reg_hi, uint8_t reg_lo, uint8_t out[HEATING_PAYLOAD_MAX_SIZE])
Encode a read ("get") of an arbitrary register into a CMD_WRITE_PRIVATE (0x20) payload.
bool heating_function_is_writable(HeatingFunction fn)
Whether the register behind a function can be written.
bool heating_function_register(HeatingFunction fn, uint8_t &register_low)
Low byte of the 0x01xx register behind a function.
@ DRYING
Drying: full power for the drying duration.
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.
@ NONE
target_fw is unknown, or its required bootloader matches bootloader_version.
IO-Homecontrol command IDs, result codes and protocol enumerations.
Pure codec for the private register protocol of 2W heaters (CMD_WRITE_PRIVATE 0x20).
Fundamental IO-Homecontrol frame and crypto size constants.
int16_t window_open
0 closed / 1 open; -1 until read.
int16_t open_window_detection
1 detection on / 0 off; -1 until read.
int16_t value_of(HeatingFunction fn) const
The stored integer value of a register (a mode, a state, a flag, a percentage).
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.
int16_t temporary_state
HeatingTemporaryState value; -1 until read.
bool clear(HeatingFunction fn)
Forget a register's value: it reads as "never read" again.
int16_t night_light
Night light 1 on / 0 off; -1 until read.
int16_t working_rate
Working rate, 0-100 %; -1 until read.
float temperature_of(HeatingFunction fn) const
The stored temperature of a register read in degrees.
float max_setpoint_c
Highest setpoint the heater accepts; NaN until read.
int16_t towel_mode
TowelDryerMode value; -1 until read.
int16_t mode
HeatingMode value; -1 until read.
int16_t night_light_brightness
Night light brightness, 0-100 %; -1 until read.
int16_t regulator
HeatingRegulatorState value; -1 until read.
float in_force_temperature_c
A towel dryer's temperature in force (register 0x0137); NaN until read.
float eco_setback_k
A towel dryer's eco setback (register 0x0104); NaN 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
uint8_t data[FRAME_MAX_DATA_SIZE]
Command parameters (0–23 bytes). Never includes mac.
Definition proto_frame.h:99
uint8_t data_len
Actual length of data.