16constexpr uint16_t LOW_BYTE_MASK = 0xFF;
19constexpr size_t PAYLOAD_LEN_HEADER = 4;
21constexpr size_t PAYLOAD_LEN_ONE_VALUE = 5;
26constexpr float DEGREES_TO_TENTHS = 10.0F;
28constexpr float HUNDREDTHS_PER_DEGREE = 100.0F;
37constexpr uint8_t WORKING_RATE_MAX = 100;
40enum class HeatingValueKind : uint8_t {
52enum class HeatingAccess : uint8_t {
62struct HeatingFunctionDescriptor {
67 HeatingValueKind kind;
71constexpr HeatingFunctionDescriptor HEATING_FUNCTIONS[] = {
74 HeatingValueKind::TENTHS_U16},
78 HeatingValueKind::BINARY},
81 HeatingValueKind::HUNDREDTHS_S16},
83 HeatingValueKind::BOUNDED_BYTE, REGULATOR_STATE_MAX},
91 HeatingValueKind::BOUNDED_BYTE, TEMPORARY_STATE_MAX},
97 HeatingValueKind::TENTHS_U16},
109 for (
const auto &d : HEATING_FUNCTIONS) {
117void write_header(
const HeatingFunctionDescriptor &d, uint8_t direction, uint8_t *out) {
121 out[3] = d.register_low;
125size_t value_length(HeatingValueKind kind) {
127 case HeatingValueKind::NONE:
129 case HeatingValueKind::TENTHS_U16:
130 case HeatingValueKind::HUNDREDTHS_S16:
131 case HeatingValueKind::BOUNDED_U16:
133 case HeatingValueKind::BINARY:
134 case HeatingValueKind::MODE:
135 case HeatingValueKind::BOUNDED_BYTE:
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);
149size_t write_u16_set(
const HeatingFunctionDescriptor &d, uint16_t raw, uint8_t *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;
159 const HeatingFunctionDescriptor *d = descriptor_for(fn);
160 return d !=
nullptr ? d->name :
"unknown";
164 const HeatingFunctionDescriptor *d = descriptor_for(fn);
165 return d !=
nullptr && d->access != HeatingAccess::READ_ONLY;
169 const HeatingFunctionDescriptor *d = descriptor_for(fn);
170 return d !=
nullptr && d->access != HeatingAccess::WRITE_ONLY;
174 const HeatingFunctionDescriptor *d = descriptor_for(fn);
177 register_low = d->register_low;
184 const uint8_t *data = reply.
data;
191 const HeatingFunctionDescriptor *d = descriptor_for(fn);
192 if (d ==
nullptr || d->access == HeatingAccess::WRITE_ONLY)
195 return PAYLOAD_LEN_HEADER;
205 return PAYLOAD_LEN_HEADER;
209 const HeatingFunctionDescriptor *d = descriptor_for(fn);
212 if (d ==
nullptr || d->access == HeatingAccess::READ_ONLY)
216 case HeatingValueKind::TENTHS_U16: {
219 if (!std::isfinite(value) || value < HEATING_TEMP_MIN_C || value >
HEATING_TEMP_MAX_C)
228 return write_u16_set(*d,
static_cast<uint16_t
>(std::round(value * DEGREES_TO_TENTHS)), out);
231 case HeatingValueKind::MODE:
238 out[4] =
static_cast<uint8_t
>(value);
239 return PAYLOAD_LEN_ONE_VALUE;
241 case HeatingValueKind::BINARY:
242 if (value != 0.0F && value != 1.0F)
245 out[4] =
static_cast<uint8_t
>(value);
246 return PAYLOAD_LEN_ONE_VALUE;
248 case HeatingValueKind::BOUNDED_BYTE:
250 if (!is_bounded_integer(value, d->max_value))
253 out[4] =
static_cast<uint8_t
>(value);
254 return PAYLOAD_LEN_ONE_VALUE;
256 case HeatingValueKind::BOUNDED_U16:
258 if (!is_bounded_integer(value, d->max_value))
260 return write_u16_set(*d,
static_cast<uint16_t
>(value), out);
262 case HeatingValueKind::NONE:
263 case HeatingValueKind::HUNDREDTHS_S16:
270 const HeatingFunctionDescriptor *d = descriptor_for(fn);
277 const HeatingFunctionDescriptor *d = descriptor_for(fn);
278 if (d ==
nullptr || d->kind == HeatingValueKind::NONE ||
285 const auto raw16 =
static_cast<uint16_t
>(v[0] | (v[1] << BITS_PER_BYTE));
287 case HeatingValueKind::TENTHS_U16:
288 value =
static_cast<float>(raw16) / DEGREES_TO_TENTHS;
291 case HeatingValueKind::HUNDREDTHS_S16: {
292 const auto signed_raw =
static_cast<int16_t
>(raw16);
297 case HeatingValueKind::BINARY:
300 value =
static_cast<float>(v[0]);
303 case HeatingValueKind::MODE:
306 value =
static_cast<float>(v[0]);
309 case HeatingValueKind::BOUNDED_BYTE:
310 if (v[0] > d->max_value)
312 value =
static_cast<float>(v[0]);
315 case HeatingValueKind::BOUNDED_U16:
316 if (raw16 > d->max_value)
318 value =
static_cast<float>(raw16);
321 case HeatingValueKind::NONE:
332struct HeatingReadingField {
334 float HeatingReadings::*temperature;
335 int16_t HeatingReadings::*
341constexpr HeatingReadingField HEATING_READING_FIELDS[] = {
360 for (
const auto &field : HEATING_READING_FIELDS) {
370 const HeatingReadingField *field = reading_field_for(fn);
371 if (field ==
nullptr)
373 if (field->temperature !=
nullptr) {
374 this->*field->temperature = value;
376 this->*field->integer =
static_cast<int16_t
>(value);
382 const HeatingReadingField *field = reading_field_for(fn);
383 if (field ==
nullptr)
385 if (field->temperature !=
nullptr) {
386 this->*field->temperature = NAN;
388 this->*field->integer = -1;
394 const HeatingReadingField *field = reading_field_for(fn);
395 if (field ==
nullptr)
397 return field->temperature !=
nullptr ? !std::isnan(this->*field->temperature) : this->*field->integer >= 0;
401 const HeatingReadingField *field = reading_field_for(fn);
402 return field !=
nullptr && field->temperature !=
nullptr ? this->*field->temperature : NAN;
407 const HeatingReadingField *field = reading_field_for(fn);
408 return field !=
nullptr && field->integer !=
nullptr ? this->*field->integer : -1;
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).
@ HEATING
The element is heating.
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 ®ister_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).
uint8_t data[FRAME_MAX_DATA_SIZE]
Command parameters (0–23 bytes). Never includes mac.
uint8_t data_len
Actual length of data.