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 IO-Homecontrol 2W heating/climate functions (CMD_WRITE_PRIVATE 0x20).
3/// @ingroup hioc_protocol
4///
5/// Byte layout cited throughout to the iohomecontrol reference implementation's Cozy 2W device
6/// code; the direction/register bytes and the 16-bit little-endian tenths-of-a-degree setpoint
7/// encoding are from the iown-homecontrol project's Atlantic/Thermor register map.
8
9#include "proto_heating.h"
10
11#include "proto_sizes.h"
12
13#include <cmath>
14
15namespace esphome {
16namespace home_io_control {
17
18namespace {
19
20/// Mask for the low byte of the 16-bit little-endian setpoint field.
21constexpr uint16_t SETPOINT_LOW_BYTE_MASK = 0xFF;
22
23/// Payload length for a function that carries no value ({prefix, direction, reg-high, reg-low}).
24constexpr size_t PAYLOAD_LEN_NO_VALUE = 4;
25/// Payload length for a function that carries one value byte (mode / presence / window).
26constexpr size_t PAYLOAD_LEN_ONE_VALUE = 5;
27/// Payload length for SET_TEMPERATURE (a 16-bit little-endian tenths-of-a-degree setpoint).
28constexpr size_t PAYLOAD_LEN_TEMPERATURE = HEATING_PAYLOAD_MAX_SIZE;
29/// Wire scale: the setpoint is round(degrees * this), little-endian 16-bit (the Atlantic/Thermor
30/// register map's 0x0103 / 0x0130 rows; the Cozy 2W reference code does the multiply but only
31/// stores the low byte).
32constexpr float DEGREES_TO_TENTHS = 10.0F;
33
34/// @brief How a function's value maps onto the payload's trailing bytes.
35enum class HeatingValueKind : uint8_t {
36 NONE, ///< No trailing value — 4-byte payload.
37 TEMPERATURE, ///< A 16-bit little-endian tenths-of-a-degree setpoint — 6-byte payload.
38 MODE, ///< One HeatingMode byte — 5-byte payload.
39 BINARY, ///< One 0/1 byte — 5-byte payload.
40};
41
42/// @brief Per-function wire descriptor.
43///
44/// A payload is {HEATING_PAYLOAD_PREFIX, direction, HEATING_REGISTER_HIGH_BYTE, register_low,
45/// [value...]}. `direction` is 0x60 (get/read) or 0x61 (set/write) and `register_low` is the low
46/// byte of the 16-bit register number, per the Atlantic/Thermor register map ("Set 0c61 01xx" /
47/// "Get 0c60 01xx").
48struct HeatingFunctionDescriptor {
49 HeatingFunction fn; ///< Function this row describes.
50 uint8_t direction; ///< Payload byte 1: 0x60 = get/read, 0x61 = set/write.
51 uint8_t register_low; ///< Payload byte 3: low byte of the 0x01xx register number.
52 HeatingValueKind kind; ///< How this function's value maps onto the trailing payload byte(s).
53};
54
55/// Direction / register-low / value-kind per function. Payload forms (iohcCozyDevice2W.cpp;
56/// registers per AtlanticThermor/README.md):
57/// POWER_ON :105 {0x0C, 0x60, 0x01, 0x2C} get reg 0x012C (paired-device list)
58/// SET_TEMPERATURE :125 {0x0C, 0x61, 0x01, 0x03, lo, hi} set reg 0x0103, LE16 tenths
59/// SET_MODE :155 {0x0C, 0x61, 0x01, 0x00, mode} set reg 0x0100
60/// SET_PRESENCE :195 {0x0C, 0x61, 0x01, 0x10, val} set reg 0x0110
61/// SET_WINDOW :218 {0x0C, 0x61, 0x01, 0x0E, val} set reg 0x010E
62/// MIDNIGHT_SYNC :248 {0x0c, 0x60, 0x01, 0x30} get reg 0x0130 (setpoint block)
63constexpr HeatingFunctionDescriptor HEATING_FUNCTIONS[] = {
64 {HeatingFunction::POWER_ON, 0x60, 0x2C, HeatingValueKind::NONE},
65 {HeatingFunction::SET_TEMPERATURE, 0x61, 0x03, HeatingValueKind::TEMPERATURE},
66 {HeatingFunction::SET_MODE, 0x61, 0x00, HeatingValueKind::MODE},
67 {HeatingFunction::SET_PRESENCE, 0x61, 0x10, HeatingValueKind::BINARY},
68 {HeatingFunction::SET_WINDOW, 0x61, 0x0E, HeatingValueKind::BINARY},
69 {HeatingFunction::MIDNIGHT_SYNC, 0x60, 0x30, HeatingValueKind::NONE},
70};
71
72/// @brief Look up the wire descriptor for a function.
73/// @param fn Function to resolve.
74/// @return Pointer into the constexpr HEATING_FUNCTIONS table, or nullptr for an out-of-range value.
75const HeatingFunctionDescriptor *descriptor_for(HeatingFunction fn) {
76 for (const auto &d : HEATING_FUNCTIONS) {
77 if (d.fn == fn)
78 return &d;
79 }
80 return nullptr;
81}
82
83} // namespace
84
86 switch (fn) {
88 return "power_on";
90 return "set_temperature";
92 return "set_mode";
94 return "set_presence";
96 return "set_window";
98 return "midnight_sync";
99 }
100 return "unknown";
101}
102
104 const HeatingFunctionDescriptor *d = descriptor_for(fn);
105 if (d == nullptr)
106 return 0;
107
108 // Common prefix — {PREFIX, direction, REGISTER_HIGH_BYTE, register_low} — shared by every
109 // function (iohcCozyDevice2W.cpp:105 / :125 / :155 / :195 / :218 / :248; direction and register
110 // per AtlanticThermor/README.md).
111 out[0] = HEATING_PAYLOAD_PREFIX;
112 out[1] = d->direction;
114 out[3] = d->register_low;
115
116 switch (d->kind) {
117 case HeatingValueKind::NONE:
118 return PAYLOAD_LEN_NO_VALUE;
119
120 case HeatingValueKind::TEMPERATURE: {
121 // Reject NaN / infinity and anything outside the representable range rather than emitting a
122 // wrapped byte (the reference's latent truncation bug, iohcCozyDevice2W.cpp:127-128).
123 if (!std::isfinite(value) || value < HEATING_TEMP_MIN_C || value > HEATING_TEMP_MAX_C)
124 return 0;
125 // Multiply in float (not double): the float32 product of e.g. 20.55f * 10.0f is exactly
126 // 205.5f, and std::round() takes that half away from zero to 206 (0x00CE). Truncating (as
127 // the reference does) or rounding toward even would both be wrong here.
128 // NB: this exactness relies on FLT_EVAL_METHOD == 0 (operands evaluated at their own
129 // single precision, no hidden widening to double). That holds on x86-64 SSE and on Xtensa;
130 // the proto_heating test pins 20.55 C, which is one ULP from flipping to 0xCD under any
131 // excess-precision evaluation.
132 // The setpoint field is a 16-bit little-endian value in tenths of a degree
133 // (AtlanticThermor/README.md's 0x0103 / 0x0130 rows show `cd 00`, `c3 00`, `18 01` = 28.0).
134 // For every value <= 25.5 C the high byte is 0x00, so the emitted bytes are identical to the
135 // single-byte form iohcCozyDevice2W.cpp:125-128 writes.
136 const auto tenths = static_cast<uint16_t>(std::round(value * DEGREES_TO_TENTHS));
137 out[4] = static_cast<uint8_t>(tenths & SETPOINT_LOW_BYTE_MASK);
138 out[PAYLOAD_LEN_TEMPERATURE - 1] = static_cast<uint8_t>(tenths >> BITS_PER_BYTE);
139 return PAYLOAD_LEN_TEMPERATURE;
140 }
141
142 case HeatingValueKind::MODE:
143 // Exact float match against the four exposed modes (iohcCozyDevice2W.cpp:158-162); a
144 // non-integral or unknown value is rejected, not coerced.
145 if (value != static_cast<float>(HeatingMode::AUTO) && value != static_cast<float>(HeatingMode::MANUAL) &&
146 value != static_cast<float>(HeatingMode::PROG) && value != static_cast<float>(HeatingMode::OFF))
147 return 0;
148 out[4] = static_cast<uint8_t>(value);
149 return PAYLOAD_LEN_ONE_VALUE;
150
151 case HeatingValueKind::BINARY:
152 // 0 or 1 only (iohcCozyDevice2W.cpp:198-199 for presence, :221-222 for window).
153 if (value != 0.0F && value != 1.0F)
154 return 0;
155 out[4] = static_cast<uint8_t>(value);
156 return PAYLOAD_LEN_ONE_VALUE;
157 }
158 return 0;
159}
160
161} // namespace home_io_control
162} // namespace esphome
static constexpr uint8_t BITS_PER_BYTE
Number of bits in one protocol byte.
Definition proto_sizes.h:27
constexpr size_t HEATING_PAYLOAD_MAX_SIZE
Largest payload any function produces — SET_TEMPERATURE's 6-byte form (iohcCozyDevice2W....
size_t encode_heating_payload(HeatingFunction fn, float value, uint8_t out[HEATING_PAYLOAD_MAX_SIZE])
Encode one heating function into a CMD_WRITE_PRIVATE (0x20) payload.
HeatingFunction
Heating functions, one per user-pressable radiator button in the reference.
@ MIDNIGHT_SYNC
Reads register 0x0130 — the comfort/eco/auto setpoint block (iohcCozyDevice2W.cpp:248; AtlanticThermo...
@ POWER_ON
Wake / retrieve paired devices (iohcCozyDevice2W.cpp:105).
@ SET_PRESENCE
Presence / absence (iohcCozyDevice2W.cpp:195).
@ SET_TEMPERATURE
Setpoint in degrees Celsius (iohcCozyDevice2W.cpp:125).
@ SET_MODE
Operating mode (iohcCozyDevice2W.cpp:155).
@ SET_WINDOW
Open-window / frost-protection (iohcCozyDevice2W.cpp:218).
constexpr uint8_t HEATING_REGISTER_HIGH_BYTE
Payload byte at index 2 — the high byte of the 16-bit register number, constant 0x01 (registers are 0...
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", ...).
@ OFF
Off / standby; note the value is 0x04, not 0x03 (that is the reference's commented-out "special" mode...
@ PROG
Program mode; device runs its own stored weekly schedule (iohcCozyDevice2W.cpp:160).
@ MANUAL
Manual mode; follows the last SET_TEMPERATURE setpoint (iohcCozyDevice2W.cpp:159).
@ AUTO
Automatic mode; device manages the setpoint itself (iohcCozyDevice2W.cpp:158).
constexpr uint8_t HEATING_PAYLOAD_PREFIX
Leading payload byte, common to every function (iohcCozyDevice2W.cpp:105 et al.).
@ NONE
target_fw is unknown, or its required bootloader matches bootloader_version.
Pure codec for IO-Homecontrol 2W heating/climate functions (CMD_WRITE_PRIVATE 0x20).
Fundamental IO-Homecontrol frame and crypto size constants.