Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
proto_heating_profiles.cpp
Go to the documentation of this file.
1/// @file proto_heating_profiles.cpp
2/// @brief The heating profile table and its lookups.
3/// @ingroup hioc_protocol
4
6
8#include "proto_sizes.h"
9
10#include <algorithm>
11#include <cmath>
12#include <iterator>
13
14namespace esphome {
15namespace home_io_control {
16
17namespace {
18
19/// Mask for the low byte of a main parameter, where a heating level sits.
20constexpr uint16_t LOW_BYTE_MASK = 0xFF;
21
22// === Electrical heater with an adjustable setpoint (52/1): private registers ===
23
24constexpr HeatingModeEntry SETPOINT_HEATER_MODES[] = {
25 {HeatingHvacMode::OFF, static_cast<uint8_t>(HeatingMode::OFF)},
26 {HeatingHvacMode::HEAT, static_cast<uint8_t>(HeatingMode::MANUAL)},
27 {HeatingHvacMode::AUTO, static_cast<uint8_t>(HeatingMode::AUTO)},
28};
29
30/// Index of the "Program" entry in SETPOINT_HEATER_PRESETS, named by SETPOINT_HEATER_OBSERVED.
31constexpr int8_t SETPOINT_HEATER_PROGRAM_PRESET = 2;
32
33constexpr HeatingPresetEntry SETPOINT_HEATER_PRESETS[] = {
34 // Presence is written to a register nothing reads back, so these show the last command.
35 {HeatingPresetId::HOME, nullptr, HeatingFunction::SET_PRESENCE, 1, true, false},
36 {HeatingPresetId::AWAY, nullptr, HeatingFunction::SET_PRESENCE, 0, true, false},
37 // The device's own weekly schedule is a mode of its own, not a presence.
38 {HeatingPresetId::CUSTOM, "Program", HeatingFunction::SET_MODE, static_cast<uint8_t>(HeatingMode::PROG), true,
39 true},
40};
41
42constexpr HeatingObservedState SETPOINT_HEATER_OBSERVED[] = {
43 {static_cast<uint8_t>(HeatingMode::AUTO), HeatingHvacMode::AUTO, -1},
44 {static_cast<uint8_t>(HeatingMode::MANUAL), HeatingHvacMode::HEAT, -1},
45 // The schedule has no HVAC mode: auto, with the preset naming it.
46 {static_cast<uint8_t>(HeatingMode::PROG), HeatingHvacMode::AUTO, SETPOINT_HEATER_PROGRAM_PRESET},
47 // The setpoint comes from another controller; heat is the nearest mode. Never written here.
48 {static_cast<uint8_t>(HeatingMode::EXTERNAL_SETPOINT), HeatingHvacMode::HEAT, -1},
49 {static_cast<uint8_t>(HeatingMode::OFF), HeatingHvacMode::OFF, -1},
50};
51
52/// The room temperature leads: it is the value a user looks at, so a poll that dies half-way still
53/// delivered it.
54constexpr HeatingFunction SETPOINT_HEATER_POLL[] = {
57};
58
59/// Properties and settings that change rarely: read until they have been read once.
60constexpr HeatingFunction SETPOINT_HEATER_BOOT[] = {
63};
64
65// === Plain electrical heater (52/0): heating levels ===
66
67constexpr HeatingModeEntry LEVEL_HEATER_MODES[] = {
68 {HeatingHvacMode::OFF, heating_level::OFF},
69 {HeatingHvacMode::HEAT, heating_level::COMFORT},
70};
71
72constexpr HeatingPresetEntry LEVEL_HEATER_PRESETS[] = {
73 // write_fn is unused on this path; SET_MODE is a placeholder that is never sent.
74 {HeatingPresetId::AWAY, nullptr, HeatingFunction::SET_MODE, heating_level::FROST_PROTECTION, true, true},
75 {HeatingPresetId::ECO, nullptr, HeatingFunction::SET_MODE, heating_level::ECO, true, true},
76 {HeatingPresetId::COMFORT, nullptr, HeatingFunction::SET_MODE, heating_level::COMFORT, true, true},
77 {HeatingPresetId::BOOST, nullptr, HeatingFunction::SET_MODE, heating_level::BOOST, true, true},
78 {HeatingPresetId::CUSTOM, "Comfort -1", HeatingFunction::SET_MODE, heating_level::COMFORT_MINUS_1, true, true},
79 {HeatingPresetId::CUSTOM, "Comfort -2", HeatingFunction::SET_MODE, heating_level::COMFORT_MINUS_2, true, true},
80 // The device reports it, a controller cannot set it.
81 {HeatingPresetId::CUSTOM, "Secured", HeatingFunction::SET_MODE, heating_level::SECURED, false, true},
82};
83
84constexpr HeatingObservedState LEVEL_HEATER_OBSERVED[] = {
85 {heating_level::OFF, HeatingHvacMode::OFF, -1},
86 {heating_level::FROST_PROTECTION, HeatingHvacMode::HEAT, 0},
87 {heating_level::ECO, HeatingHvacMode::HEAT, 1},
88 {heating_level::COMFORT, HeatingHvacMode::HEAT, 2},
89 {heating_level::BOOST, HeatingHvacMode::HEAT, 3},
90 {heating_level::COMFORT_MINUS_1, HeatingHvacMode::HEAT, 4},
91 {heating_level::COMFORT_MINUS_2, HeatingHvacMode::HEAT, 5},
92 {heating_level::SECURED, HeatingHvacMode::HEAT, 6},
93};
94
95// === Heating temperature interface (14/0) and heat pump (22/0): temperature in the main parameter ===
96
97constexpr HeatingFp NO_FP{};
98constexpr HeatingFp THERMOSTAT_MANUAL{HeatingFpKind::FP9, heating_mode_flags::MANUAL};
99constexpr HeatingFp HEAT_PUMP_ON{HeatingFpKind::FP16, heating_fp16::ON};
100
101/// Thermostat modes as the device's own controller sends them: off is the off marker with FP9 0, auto is
102/// the comfort alias with FP9 1, heat (manual) is the current target temperature with FP9 2.
103constexpr HeatingModeEntry TEMPERATURE_INTERFACE_MODES[] = {
104 {HeatingHvacMode::OFF, 0, HEATING_OFF_MP, {HeatingFpKind::FP9, heating_mode_flags::OFF}},
105 {HeatingHvacMode::HEAT, 0, HEATING_MP_FROM_TARGET, THERMOSTAT_MANUAL},
107 0,
108 heating_prefixed_main_parameter(HEATING_ALIAS_MP_PREFIX, heating_alias::COMFORT),
109 {HeatingFpKind::FP9, heating_mode_flags::AUTO}},
110};
111
112/// A setpoint alias is selected by its main parameter alone, with no functional parameter.
113constexpr HeatingPresetEntry TEMPERATURE_INTERFACE_PRESETS[] = {
114 {HeatingPresetId::COMFORT, nullptr, HeatingFunction::SET_MODE, heating_alias::COMFORT, true, true, NO_FP},
115 {HeatingPresetId::ECO, nullptr, HeatingFunction::SET_MODE, heating_alias::THERMOSTAT_ECO, true, true, NO_FP},
116 {HeatingPresetId::CUSTOM, "Secured", HeatingFunction::SET_MODE, heating_alias::SECURED, true, true, NO_FP},
117};
118
119/// The mode is the last command (nothing reads it back), so the hvac column is not consulted; the rows
120/// only name the preset an alias stands for.
121constexpr HeatingObservedState TEMPERATURE_INTERFACE_OBSERVED[] = {
122 {heating_alias::COMFORT, HeatingHvacMode::HEAT, 0},
123 {heating_alias::THERMOSTAT_ECO, HeatingHvacMode::HEAT, 1},
124 {heating_alias::SECURED, HeatingHvacMode::HEAT, 2},
125};
126
127/// Heat pump: off is the off marker with FP16 off; on is the comfort alias with FP16 on, which is also
128/// what a preset sends. No mode selects cooling.
129constexpr HeatingModeEntry HEAT_PUMP_MODES[] = {
130 {HeatingHvacMode::OFF, 0, HEATING_OFF_MP, {HeatingFpKind::FP16, heating_fp16::OFF}},
131 {HeatingHvacMode::HEAT, 0, heating_prefixed_main_parameter(HEATING_ALIAS_MP_PREFIX, heating_alias::COMFORT),
132 HEAT_PUMP_ON},
133};
134
135constexpr HeatingPresetEntry HEAT_PUMP_PRESETS[] = {
136 {HeatingPresetId::COMFORT, nullptr, HeatingFunction::SET_MODE, heating_alias::COMFORT, true, true, HEAT_PUMP_ON},
137 {HeatingPresetId::ECO, nullptr, HeatingFunction::SET_MODE, heating_alias::HEAT_PUMP_ECO, true, true, HEAT_PUMP_ON},
138 {HeatingPresetId::CUSTOM, "Setback", HeatingFunction::SET_MODE, heating_alias::HEAT_PUMP_SETBACK, true, true,
139 HEAT_PUMP_ON},
140 {HeatingPresetId::CUSTOM, "Halted", HeatingFunction::SET_MODE, heating_alias::HEAT_PUMP_HALTED, true, true,
141 HEAT_PUMP_ON},
142};
143
144constexpr HeatingObservedState HEAT_PUMP_OBSERVED[] = {
145 {heating_alias::COMFORT, HeatingHvacMode::HEAT, 0},
146 {heating_alias::HEAT_PUMP_ECO, HeatingHvacMode::HEAT, 1},
147 {heating_alias::HEAT_PUMP_SETBACK, HeatingHvacMode::HEAT, 2},
148 {heating_alias::HEAT_PUMP_HALTED, HeatingHvacMode::HEAT, 3},
149};
150
151constexpr float TEMPERATURE_VISUAL_MIN_C = 5.0F; ///< Lowest target the card offers for a temperature profile.
152constexpr float TEMPERATURE_VISUAL_MAX_C = 30.0F; ///< Highest target the card offers for a temperature profile.
153
154constexpr HeatingProfile HEATING_PROFILES[] = {
156 0,
158 {/*target_temperature=*/true, /*current_temperature=*/false, /*action=*/false, /*window_sensor=*/false,
159 /*open_window_detection=*/false, /*mode_observed=*/false},
160 HEATING_ALIAS_MP_PREFIX,
161 TEMPERATURE_VISUAL_MIN_C,
162 TEMPERATURE_VISUAL_MAX_C,
163 THERMOSTAT_MANUAL,
164 /*fixed_range=*/false,
165 TEMPERATURE_INTERFACE_MODES,
166 std::size(TEMPERATURE_INTERFACE_MODES),
167 TEMPERATURE_INTERFACE_PRESETS,
168 std::size(TEMPERATURE_INTERFACE_PRESETS),
169 TEMPERATURE_INTERFACE_OBSERVED,
170 std::size(TEMPERATURE_INTERFACE_OBSERVED),
171 nullptr,
172 0,
173 nullptr,
174 0},
176 0,
178 {/*target_temperature=*/true, /*current_temperature=*/false, /*action=*/false, /*window_sensor=*/false,
179 /*open_window_detection=*/false, /*mode_observed=*/false},
180 HEATING_ALIAS_MP_PREFIX,
181 TEMPERATURE_VISUAL_MIN_C,
182 TEMPERATURE_VISUAL_MAX_C,
183 NO_FP,
184 /*fixed_range=*/true,
185 HEAT_PUMP_MODES,
186 std::size(HEAT_PUMP_MODES),
187 HEAT_PUMP_PRESETS,
188 std::size(HEAT_PUMP_PRESETS),
189 HEAT_PUMP_OBSERVED,
190 std::size(HEAT_PUMP_OBSERVED),
191 nullptr,
192 0,
193 nullptr,
194 0},
196 0,
198 {/*target_temperature=*/false, /*current_temperature=*/false, /*action=*/false, /*window_sensor=*/false,
199 /*open_window_detection=*/false, /*mode_observed=*/true},
200 HEATING_LEVEL_MP_PREFIX,
201 0.0F,
202 0.0F,
203 NO_FP,
204 /*fixed_range=*/false,
205 LEVEL_HEATER_MODES,
206 std::size(LEVEL_HEATER_MODES),
207 LEVEL_HEATER_PRESETS,
208 std::size(LEVEL_HEATER_PRESETS),
209 LEVEL_HEATER_OBSERVED,
210 std::size(LEVEL_HEATER_OBSERVED),
211 nullptr,
212 0,
213 nullptr,
214 0},
216 1,
218 {/*target_temperature=*/true, /*current_temperature=*/true, /*action=*/true, /*window_sensor=*/true,
219 /*open_window_detection=*/true, /*mode_observed=*/true},
220 0,
223 NO_FP,
224 /*fixed_range=*/false,
225 SETPOINT_HEATER_MODES,
226 std::size(SETPOINT_HEATER_MODES),
227 SETPOINT_HEATER_PRESETS,
228 std::size(SETPOINT_HEATER_PRESETS),
229 SETPOINT_HEATER_OBSERVED,
230 std::size(SETPOINT_HEATER_OBSERVED),
231 SETPOINT_HEATER_POLL,
232 std::size(SETPOINT_HEATER_POLL),
233 SETPOINT_HEATER_BOOT,
234 std::size(SETPOINT_HEATER_BOOT)},
235};
236
237const HeatingProfile *find_exact_profile(DeviceType type, uint8_t subtype) {
238 for (const HeatingProfile &profile : HEATING_PROFILES) {
239 if (profile.type == type && profile.subtype == subtype)
240 return &profile;
241 }
242 return nullptr;
243}
244
245} // namespace
246
247const HeatingProfile *find_heating_profile(DeviceType type, uint8_t subtype) {
248 const HeatingProfile *profile = find_exact_profile(type, subtype);
249 if (profile != nullptr || subtype == 0)
250 return profile;
251 // A subtype the type table names on its own (the Atlantic heat pumps, 22/1 and 22/2) is a different
252 // device from the type's own row and must not inherit its profile; only an unnamed subtype does.
253 return find_exact_device_type_info(type, subtype) == nullptr ? find_exact_profile(type, 0) : nullptr;
254}
255
256bool device_uses_heating_registers(DeviceType type, uint8_t subtype) {
257 const HeatingProfile *profile = find_heating_profile(type, subtype);
258 return profile != nullptr && profile->path == HeatingControlPath::PRIVATE_REGISTERS;
259}
260
261bool device_uses_heating_levels(DeviceType type, uint8_t subtype) {
262 const HeatingProfile *profile = find_heating_profile(type, subtype);
263 return profile != nullptr && profile->path == HeatingControlPath::HEATING_LEVEL;
264}
265
266bool device_uses_heating_temperature(DeviceType type, uint8_t subtype) {
267 const HeatingProfile *profile = find_heating_profile(type, subtype);
268 return profile != nullptr && profile->path == HeatingControlPath::TEMPERATURE_EXECUTE;
269}
270
271bool encode_temperature_main_parameter(float celsius, float min_c, float max_c, uint16_t &main_parameter) {
272 if (!std::isfinite(celsius) || !std::isfinite(min_c) || !std::isfinite(max_c) || !(max_c > min_c))
273 return false;
274 const float steps = std::round((celsius - min_c) / (max_c - min_c) * static_cast<float>(HEATING_TEMPERATURE_MP_SPAN));
275 main_parameter = static_cast<uint16_t>(std::clamp(steps, 0.0F, static_cast<float>(HEATING_TEMPERATURE_MP_SPAN)));
276 return true;
277}
278
279bool decode_temperature_main_parameter(uint16_t main_parameter, float min_c, float max_c, float &celsius) {
280 if (main_parameter > HEATING_TEMPERATURE_MP_SPAN || !std::isfinite(min_c) || !std::isfinite(max_c) ||
281 !(max_c > min_c))
282 return false;
283 celsius =
284 min_c + (static_cast<float>(main_parameter) / static_cast<float>(HEATING_TEMPERATURE_MP_SPAN) * (max_c - min_c));
285 return true;
286}
287
288bool decode_heating_level(uint16_t main_parameter, uint8_t &level) {
289 if ((main_parameter >> BITS_PER_BYTE) != HEATING_LEVEL_MP_PREFIX)
290 return false;
291 level = static_cast<uint8_t>(main_parameter & LOW_BYTE_MASK);
292 return true;
293}
294
295const HeatingObservedState *find_observed_state(const HeatingProfile &profile, uint8_t wire) {
296 for (size_t i = 0; i < profile.observed_count; i++) {
297 if (profile.observed[i].wire == wire)
298 return &profile.observed[i];
299 }
300 return nullptr;
301}
302
303} // namespace home_io_control
304} // namespace esphome
const HeatingObservedState * find_observed_state(const HeatingProfile &profile, uint8_t wire)
Find what a device-reported wire value shows on the card.
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.
@ FP9
Functional parameter 9, one byte.
@ FP16
Functional parameter 16, two bytes.
HeatingFunction
Registers and functions of a 2W heater, one row of the descriptor table each.
@ 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.
@ 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).
const HeatingProfile * find_heating_profile(DeviceType type, uint8_t subtype)
Look up the heating profile for a (type, subtype).
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...
@ CUSTOM
Named by HeatingPresetEntry::custom_name.
@ AWAY
Nobody is in the room (the frost-protection level on a level heater).
constexpr float HEATING_TEMP_MAX_C
Highest setpoint this codec will encode.
DeviceType
IO-Homecontrol node types.
@ HEATING_TEMPERATURE_INTERFACE
Heating temperature interface.
@ TEMPERATURE_EXECUTE
Standard EXECUTE with a temperature or an alias as the main parameter; read with 0x03.
@ PRIVATE_REGISTERS
CMD_WRITE_PRIVATE register reads and writes (proto_heating.h).
@ HEATING_LEVEL
Standard EXECUTE with a heating level as the main parameter; read with 0x03.
const DeviceTypeInfo * find_exact_device_type_info(DeviceType type, uint8_t subtype)
Look up the table row with exactly this (type, subtype), without falling back.
constexpr float HEATING_TEMP_MIN_C
Lowest setpoint this codec will encode.
bool device_uses_heating_levels(DeviceType type, uint8_t subtype)
Does a climate entity drive this device with heating levels?
bool decode_heating_level(uint16_t main_parameter, uint8_t &level)
Read a heating level out of a main parameter.
@ 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.
bool device_uses_heating_registers(DeviceType type, uint8_t subtype)
Does a climate entity drive this device over the private register protocol?
bool device_uses_heating_temperature(DeviceType type, uint8_t subtype)
Does a climate entity drive this device with a temperature or an alias in the main parameter?
constexpr uint16_t heating_prefixed_main_parameter(uint8_t prefix, uint8_t low)
Main parameter of a prefixed value (a level or an alias).
The IO node-type and subtype table: names, entity class, platform and behaviour flags.
Heating profiles: what a climate card offers for each kind of heater, and how.
Fundamental IO-Homecontrol frame and crypto size constants.
A functional parameter and its value.
One HVAC mode the card offers and the wire value that selects it.
What a device-reported wire value shows on the card.
uint8_t wire
Mode register value, or level low byte, as the device reports it.
const HeatingObservedState * observed
Device-reported value -> card state.
HeatingControlPath path
How the heater is commanded and read.
size_t observed_count
Number of entries in observed.