Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
proto_dhw.cpp
Go to the documentation of this file.
1/// @file proto_dhw.cpp
2/// @brief Pure codec for a domestic hot water heater: its family-0x00 registers and its target temperature.
3/// @ingroup hioc_protocol
4
5#include "proto_dhw.h"
6
7#include "proto_constants.h"
9#include "proto_sizes.h"
10
11namespace esphome {
12namespace home_io_control {
13
14namespace {
15
16/// Length of a register request header: prefix, direction, register high byte, register low byte.
17constexpr size_t DHW_HEADER_LEN = 4;
18/// Length of a one-byte register write (mode, durations): the header plus one value byte.
19constexpr size_t DHW_BYTE_WRITE_LEN = 5;
20/// Value bytes each typed decoder needs.
21constexpr size_t DHW_WATER_TEMPERATURE_LEN = 2;
22constexpr size_t DHW_ENERGY_LEN = 4;
23constexpr size_t DHW_HEATING_STATE_LEN = 5;
24/// Byte positions inside the value of register 0x0021.
25constexpr size_t DHW_HEATING_BYTE = 0;
26constexpr size_t DHW_ELEMENT_BYTE = 2;
27constexpr size_t DHW_COMPRESSOR_BYTE = 3;
28constexpr size_t DHW_FAN_BYTE = 4;
29/// Tenths of a degree per degree in register 0x0017.
30constexpr float DHW_TENTHS_PER_DEGREE = 10.0F;
31/// Status reply layout: byte offsets of the FPI1 and FPI2 echoes, and the length of the part before the values.
32constexpr size_t DHW_STATUS_FPI1_OFFSET = 4;
33constexpr size_t DHW_STATUS_FPI2_OFFSET = 5;
34constexpr size_t DHW_STATUS_VALUES_OFFSET = 6;
35/// Bytes per functional parameter value (FP9-FP16 are 16 bits wide).
36constexpr size_t DHW_FP_VALUE_LEN = 2;
37/// FPI2 bit of FP15 and FP16.
38constexpr uint8_t DHW_FPI2_FP15_BIT = fp_bitmap_bit(DHW_FP_CAPABILITIES);
39constexpr uint8_t DHW_FPI2_FP16_BIT = fp_bitmap_bit(DHW_FP_OPERATING_MODE);
40/// FP16 fields: two bits each, 1 = on (0 keep, 2 off).
41constexpr uint16_t DHW_FP16_FIELD_MASK = 0x3;
42constexpr uint16_t DHW_FP16_FIELD_ON = 1;
43constexpr uint8_t DHW_FP16_BOOST_SHIFT = 8; ///< The relaunch field: bits 8-9.
44constexpr uint8_t DHW_FP16_AWAY_SHIFT = 10; ///< The absence field: bits 10-11.
45/// One row per DhwOperatingFeature, in its order.
46constexpr DhwOperatingFeatureWire DHW_OPERATING_FEATURES[] = {
47 {DHW_CAPABILITY_BOOST, DHW_FP16_BOOST_SHIFT, DHW_OPERATING_MODE_BOOST_ON, DHW_REG_BOOST_DURATION,
48 DHW_BOOST_START_DURATION},
49 {DHW_CAPABILITY_AWAY, DHW_FP16_AWAY_SHIFT, DHW_OPERATING_MODE_AWAY_ON, DHW_REG_AWAY_DURATION,
50 DHW_AWAY_UNTIL_TURNED_OFF},
51};
52static_assert(sizeof(DHW_OPERATING_FEATURES) / sizeof(DHW_OPERATING_FEATURES[0]) ==
53 static_cast<size_t>(DhwOperatingFeature::AWAY) + 1,
54 "one row per DhwOperatingFeature");
55/// Highest DhwMode value.
56constexpr uint8_t DHW_MODE_MAX = static_cast<uint8_t>(DhwMode::MANUAL_ECO_OFF);
57
58/// Two bytes, high byte first: every 16-bit value of this codec (temperature, main parameter, FP15, FP16).
59uint16_t read_be16(const uint8_t *bytes) { return static_cast<uint16_t>((bytes[0] << BITS_PER_BYTE) | bytes[1]); }
60
61/// Whether the two-bit FP16 field of `which` reads "on".
62bool fp16_field_on(uint16_t operating_mode, DhwOperatingFeature which) {
63 return ((operating_mode >> dhw_operating_feature_wire(which).fp16_shift) & DHW_FP16_FIELD_MASK) == DHW_FP16_FIELD_ON;
64}
65
66/// @brief Encode a one-byte write of a family-0x00 register: `0C 61 00 <index> <value>`.
67size_t encode_dhw_byte_write(uint8_t index, uint8_t value, uint8_t out[HEATING_PAYLOAD_MAX_SIZE]) {
69 out[1] = HEATING_DIRECTION_SET;
70 out[2] = DHW_REGISTER_HIGH_BYTE;
71 out[3] = index;
72 out[DHW_HEADER_LEN] = value;
73 return DHW_BYTE_WRITE_LEN;
74}
75
76} // namespace
77
78size_t encode_dhw_register_read(uint8_t index, uint8_t out[HEATING_PAYLOAD_MAX_SIZE]) {
79 return encode_heating_register_read(DHW_REGISTER_HIGH_BYTE, index, out);
80}
81
82size_t encode_dhw_mode_write(uint8_t mode, uint8_t out[HEATING_PAYLOAD_MAX_SIZE]) {
83 if (mode > DHW_MODE_MAX)
84 return 0;
85 return encode_dhw_byte_write(DHW_REG_MODE, mode, out);
86}
87
89 return DHW_OPERATING_FEATURES[static_cast<size_t>(which)];
90}
91
94 return encode_dhw_byte_write(wire.duration_register, wire.start_duration, out);
95}
96
97bool encode_dhw_target(float celsius, uint16_t &main_parameter) {
98 // encode_temperature_main_parameter() clamps; a target outside the heater's own range is refused instead.
99 if (!std::isfinite(celsius) || celsius < DHW_TARGET_MIN_C || celsius > DHW_TARGET_MAX_C)
100 return false;
101 return encode_temperature_main_parameter(celsius, DHW_TARGET_MIN_C, DHW_TARGET_MAX_C, main_parameter);
102}
103
104bool decode_dhw_register_reply(const IoFrame &reply, uint8_t index, const uint8_t *&value, size_t &len) {
105 if (!heating_reply_echoes_register(reply, DHW_REGISTER_HIGH_BYTE, index))
106 return false;
107 value = reply.data + HEATING_REPLY_VALUE_OFFSET;
109 return true;
110}
111
112bool decode_dhw_water_temperature(const uint8_t *value, size_t len, float &celsius) {
113 if (value == nullptr || len < DHW_WATER_TEMPERATURE_LEN)
114 return false;
115 const auto raw = static_cast<int16_t>(read_be16(value));
116 celsius = static_cast<float>(raw) / DHW_TENTHS_PER_DEGREE;
117 return true;
118}
119
120bool decode_dhw_mode(const uint8_t *value, size_t len, DhwMode &mode) {
121 if (value == nullptr || len < 1 || value[0] > DHW_MODE_MAX)
122 return false;
123 mode = static_cast<DhwMode>(value[0]);
124 return true;
125}
126
127bool decode_dhw_heating_state(const uint8_t *value, size_t len, DhwHeatingState &state) {
128 if (value == nullptr || len < DHW_HEATING_STATE_LEN)
129 return false;
130 state.heating = value[DHW_HEATING_BYTE] != 0;
131 state.element = value[DHW_ELEMENT_BYTE] != 0;
132 state.heat_pump = value[DHW_COMPRESSOR_BYTE] != 0 || value[DHW_FAN_BYTE] != 0;
133 return true;
134}
135
136bool decode_dhw_energy_wh(const uint8_t *value, size_t len, uint32_t &wh) {
137 if (value == nullptr || len < DHW_ENERGY_LEN)
138 return false;
139 wh = (static_cast<uint32_t>(value[0]) << (3U * BITS_PER_BYTE)) |
140 (static_cast<uint32_t>(value[1]) << (2U * BITS_PER_BYTE)) | (static_cast<uint32_t>(value[2]) << BITS_PER_BYTE) |
141 static_cast<uint32_t>(value[3]);
142 return true;
143}
144
145bool decode_dhw_duration(const uint8_t *value, size_t len, uint8_t &duration) {
146 if (value == nullptr || len < 1)
147 return false;
148 duration = value[0];
149 return true;
150}
151
152bool decode_dhw_target(uint16_t main_parameter, float &celsius) {
153 float raw = NAN;
154 if (!decode_temperature_main_parameter(main_parameter, DHW_TARGET_MIN_C, DHW_TARGET_MAX_C, raw))
155 return false;
156 // One step of the scale is under a thousandth of a degree, so the heater's 60 C reads 59.9998 unrounded;
157 // the display shows whole degrees, and tenths keep any finer setting visible.
158 celsius = std::round(raw * DHW_TENTHS_PER_DEGREE) / DHW_TENTHS_PER_DEGREE;
159 return true;
160}
161
162bool decode_dhw_status_target(const IoFrame &reply, float &celsius) {
163 if (reply.cmd != CMD_PRIVATE_RESP || reply.data_len < DHW_STATUS_MAIN_PARAMETER_OFFSET + 2)
164 return false;
165 return decode_dhw_target(read_be16(reply.data + DHW_STATUS_MAIN_PARAMETER_OFFSET), celsius);
166}
167
169 if (reply.cmd != CMD_PRIVATE_RESP || reply.data_len < DHW_STATUS_VALUES_OFFSET)
170 return false;
171 const uint8_t fpi2 = reply.data[DHW_STATUS_FPI2_OFFSET];
172 if (reply.data[DHW_STATUS_FPI1_OFFSET] != DHW_STATUS_FPI1 || (fpi2 & DHW_FPI2_FP15_BIT) == 0 ||
173 (fpi2 & DHW_FPI2_FP16_BIT) == 0)
174 return false;
175
176 // One two-byte value per FPI2 bit, bit 7 (FP9) first: walk the bits, keeping FP15 and FP16.
177 size_t offset = DHW_STATUS_VALUES_OFFSET;
178 uint16_t capabilities = 0;
179 uint16_t operating_mode = 0;
180 for (int bit = BITS_PER_BYTE - 1; bit >= 0; bit--) {
181 if ((fpi2 & (1U << bit)) == 0)
182 continue;
183 if (offset + DHW_FP_VALUE_LEN > reply.data_len)
184 return false;
185 const uint16_t value = read_be16(reply.data + offset);
186 offset += DHW_FP_VALUE_LEN;
187 if ((1U << bit) == DHW_FPI2_FP15_BIT) {
188 capabilities = value;
189 } else if ((1U << bit) == DHW_FPI2_FP16_BIT) {
190 operating_mode = value;
191 }
192 }
193
194 DhwOperatingStatus decoded;
195 decoded.capabilities = capabilities;
196 decoded.boost = fp16_field_on(operating_mode, DhwOperatingFeature::BOOST);
197 decoded.away = fp16_field_on(operating_mode, DhwOperatingFeature::AWAY);
198 status = decoded;
199 return true;
200}
201
202} // namespace home_io_control
203} // 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_dhw_mode(const uint8_t *value, size_t len, DhwMode &mode)
Operating mode from the value of register 0x0006.
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).
bool encode_temperature_main_parameter(float celsius, float min_c, float max_c, uint16_t &main_parameter)
Encode a temperature as a main parameter over an encoding range.
DhwMode
Operating mode as register 0x0006 holds it.
Definition proto_dhw.h:145
@ MANUAL_ECO_OFF
Manual mode with ECO off (the electric element may help).
Definition proto_dhw.h:148
bool decode_temperature_main_parameter(uint16_t main_parameter, float min_c, float max_c, float &celsius)
Decode a main parameter as a temperature over an encoding range (the inverse of encode_temperature_ma...
bool decode_dhw_heating_state(const uint8_t *value, size_t len, DhwHeatingState &state)
What is heating, from the value of register 0x0021.
DhwOperatingFeature
The two features the operating-mode word switches; dhw_operating_feature_wire() holds their wire fact...
Definition proto_dhw.h:115
bool encode_dhw_target(float celsius, uint16_t &main_parameter)
Encode a target temperature as a main parameter over DHW_TARGET_MIN_C..DHW_TARGET_MAX_C.
Definition proto_dhw.cpp:97
bool decode_dhw_operating_status(const IoFrame &reply, DhwOperatingStatus &status)
Capabilities and operating-mode state from the reply to the status read DHW_STATUS_FPI1 / DHW_STATUS_...
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?
bool decode_dhw_water_temperature(const uint8_t *value, size_t len, float &celsius)
Water temperature from the value of register 0x0017: signed 16-bit big-endian tenths.
constexpr uint8_t HEATING_DIRECTION_SET
Payload byte at index 1 of a write ("set") request.
size_t encode_dhw_register_read(uint8_t index, uint8_t out[HEATING_PAYLOAD_MAX_SIZE])
Encode a read of one family-0x00 register: 0C 60 00 <index>.
Definition proto_dhw.cpp:78
bool decode_dhw_energy_wh(const uint8_t *value, size_t len, uint32_t &wh)
Energy since installation from the value of register 0x0004: unsigned 32-bit big-endian,...
constexpr uint8_t fp_bitmap_bit(uint8_t fp)
Bit of functional parameter fp (1-16) in its bitmap: FPI1 holds FP1-FP8 and FPI2 FP9-FP16,...
size_t encode_dhw_duration_write(DhwOperatingFeature which, uint8_t out[HEATING_PAYLOAD_MAX_SIZE])
Encode the duration write a start of which sends before its FP16 EXECUTE: 0C 61 00 00 01 for boost (D...
Definition proto_dhw.cpp:92
bool decode_dhw_register_reply(const IoFrame &reply, uint8_t index, const uint8_t *&value, size_t &len)
The value bytes of a reply to a family-0x00 register read.
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 decode_dhw_target(uint16_t main_parameter, float &celsius)
Target temperature from a main parameter.
size_t encode_dhw_mode_write(uint8_t mode, uint8_t out[HEATING_PAYLOAD_MAX_SIZE])
Encode a write of the operating mode: 0C 61 00 06 <mode>.
Definition proto_dhw.cpp:82
const DhwOperatingFeatureWire & dhw_operating_feature_wire(DhwOperatingFeature which)
The wire facts of which: the one place each feature's FP15 bit, FP16 field and duration live.
Definition proto_dhw.cpp:88
bool decode_dhw_status_target(const IoFrame &reply, float &celsius)
Target temperature from a status reply (CMD_PRIVATE_RESP) of a domestic hot water heater.
bool decode_dhw_duration(const uint8_t *value, size_t len, uint8_t &duration)
A duration from the value of register 0x0000 or 0x0005: one byte.
IO-Homecontrol command IDs, result codes and protocol enumerations.
Pure codec for a domestic hot water heater: its family-0x00 registers and its target temperature.
Heating profiles: what a climate card offers for each kind of heater, and how.
Fundamental IO-Homecontrol frame and crypto size constants.
What register 0x0021 says is heating.
Definition proto_dhw.h:152
bool heat_pump
Bytes 3 and 4: the heat pump runs (both were always set together).
Definition proto_dhw.h:155
bool element
Byte 2: the electric element is on (set during boost only).
Definition proto_dhw.h:154
bool heating
Byte 0: the heater is heating at all (heat pump, element or both).
Definition proto_dhw.h:153
The wire facts of one DhwOperatingFeature: what FP15, FP16 and the duration registers say about it.
Definition proto_dhw.h:121
uint8_t start_duration
Duration a start writes before the FP16 EXECUTE.
Definition proto_dhw.h:126
uint8_t duration_register
Register that holds its duration (DHW_REG_*_DURATION).
Definition proto_dhw.h:125
The operating-mode part of the status read DHW_STATUS_FPI1 / DHW_STATUS_FPI2 (its main parameter,...
Definition proto_dhw.h:136
bool boost
FP16 relaunch field (bits 8-9) is 1: a boost runs.
Definition proto_dhw.h:138
bool away
FP16 absence field (bits 10-11) is 1: the holiday mode is on.
Definition proto_dhw.h:139
uint16_t capabilities
FP15: DHW_CAPABILITY_* bits.
Definition proto_dhw.h:137
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.