Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
platform_climate.h
Go to the documentation of this file.
1#pragma once
2
3/// @file platform_climate.h
4/// @brief Experimental climate entity for IO-Homecontrol heaters, driven by a heating profile.
5/// @ingroup hioc_platforms
6///
7/// Maps Home Assistant climate semantics (HVAC mode, target temperature, presets) onto the device's
8/// heating profile (proto_heating_profiles.h). The profile says what the card offers and how each
9/// choice reaches the device: as private registers (proto_heating.h, sent through
10/// IOHomeControlComponent::send_heating_command() and read_heating_register()) or as a heating level
11/// in the standard EXECUTE (IOHomeControlComponent::set_heating_level()). This class has no
12/// per-device-type branch; the `heating_control` hub action shares the register send path.
13///
14/// State is observed, not predicted. The hub polls the heater (default every 5 minutes) and this
15/// entity publishes from IoDevice::heating, which only a decoded reply changes. After an accepted
16/// register write the register is read back; after a level write the hub's settle poll reads the
17/// level. Until the first read succeeds the entity shows the last commanded mode, setpoint or preset,
18/// and nothing else. The entity deliberately does NOT use the OptimisticState overlay (ADR 0030) —
19/// there is never a prediction to keep apart from an observation (ADR 0054, ADR 0055).
20///
21/// The register path is confirmed on an Atlantic AGILIA IO radiator: setpoint and mode writes, their
22/// read-back, and the poll's room temperature and regulator readings
23/// ([issue #165](https://github.com/laberning/home_io_control/issues/165), corpus
24/// tests/corpus/captures/exchange/atlantic_agilia_exchange_status_read.yaml).
25///
26/// @todo The heating-level and temperature EXECUTE paths are unconfirmed on hardware; confirm with a
27/// capture from a plain heater, a heating temperature interface or a heat pump.
28///
29/// @warning The EXECUTE-commanded profiles are unvalidated on hardware. See
30/// docs/configuration/climate.md.
31
32#include "esphome/components/climate/climate.h"
33#include "esphome/core/component.h"
35#include "proto_heating.h"
37
38#include <vector>
39
40namespace esphome {
41namespace home_io_control {
42
43/// @brief Climate entity for IO-Homecontrol heating devices.
44/// @ingroup hioc_platforms
45///
46/// The device-binding setters and setup() registration ritual come from DeviceBoundEntity; the
47/// profile-to-ESPHome mapping and the publish-from-readings discipline live here.
48class IOHomeClimate : public climate::Climate, public Component, public DeviceBoundEntity {
49 public:
50 /// @brief Register the entity with the shared hub and schedule the first poll of the heater.
51 void setup() override;
52 /// @brief Dump configuration to the log.
53 void dump_config() override;
54 /// @brief Get setup priority (DATA).
55 /// @return setup_priority::DATA.
56 [[nodiscard]] float get_setup_priority() const override { return setup_priority::DATA; }
57
58 /// @brief Traits from the device's heating profile: its HVAC modes and presets, current temperature
59 /// and action where the profile has them, and a target range from HEATING_TEMP_MIN_C to the
60 /// heater's own maximum setpoint (HEATING_TEMP_MAX_C until it has been read) at a 0.5 C step. A
61 /// device whose type is not known yet gets the off mode only.
62 climate::ClimateTraits traits() override;
63
64 /// @brief Apply a Home Assistant climate control request.
65 ///
66 /// Each requested mode, preset and setpoint is looked up in the profile and sent over its control
67 /// path. After an accepted register write the register is read back; what the heater answers is
68 /// what the entity then shows. Only while no read has succeeded yet does an accepted write publish
69 /// the commanded value. A failed send leaves the entity state untouched.
70 /// @param call The requested attribute changes.
71 void control(const climate::ClimateCall &call) override;
72
73 /// @brief Override the temperature encoding range (YAML `min_temperature:` / `max_temperature:`) of a
74 /// heater that carries a temperature in its main parameter; applied to the device in setup().
75 /// @param min_c Lower end of the range in degrees Celsius.
76 /// @param max_c Upper end of the range.
77 void set_temperature_range(float min_c, float max_c) {
78 this->range_min_c_ = min_c;
79 this->range_max_c_ = max_c;
80 this->has_range_ = true;
81 }
82
83 protected:
84 /// @brief The device's heating profile: from the declared type, else from the type the hub learned.
85 /// @return The profile, or nullptr while the type is unknown.
86 [[nodiscard]] const HeatingProfile *profile_() const;
87
88 /// @brief Inbound device-update hook: fold IoDevice::heating into the entity and publish when
89 /// something it shows changed. Ignores updates for other devices.
90 void on_device_update_(const std::string &id, const IoDevice &dev);
91
92 /// @brief Copy the readings into the entity's fields.
93 /// @param profile The device's profile.
94 /// @param readings What the hub has read from the heater.
95 /// @return true when a published field (including the traits' maximum) changed.
96 bool sync_from_readings_(const HeatingProfile &profile, const HeatingReadings &readings);
97
98 /// @brief Fold the temperatures (current, target, maximum) of the readings into the entity.
99 /// @param profile The device's profile.
100 /// @param readings What the hub has read from the heater.
101 /// @param target_c The target the card shows: the comfort setpoint, or the temperature in force while the
102 /// heater's own programme runs; NaN when none has been read.
103 /// @param[in,out] seen Set when a reading was present.
104 /// @return true when a published field changed.
105 bool sync_temperatures_(const HeatingProfile &profile, const HeatingReadings &readings, float target_c, bool &seen);
106
107 /// @brief Fold the profile's action source (the regulator state, or the working rate) into the entity's
108 /// action.
109 /// @param profile The device's profile.
110 /// @param readings What the hub has read from the heater.
111 /// @param mode_known True when the mode or level has been read (an off heater is then not idle).
112 /// @param[in,out] seen Set when a reading was present.
113 /// @return true when the action changed.
114 bool sync_action_(const HeatingProfile &profile, const HeatingReadings &readings, bool mode_known, bool &seen);
115
116 /// @brief Show a device-reported mode register value or heating level.
117 /// @details Sets the preset too, unless the profile keeps its preset in a register of its own
118 /// (HeatingProfile::preset_observed), which apply_observed_preset_() shows.
119 /// @param profile The device's profile.
120 /// @param wire Value as the device reports it.
121 /// @return true when the mode or a preset changed; a value the profile does not map changes nothing.
122 bool apply_observed_state_(const HeatingProfile &profile, uint8_t wire);
123
124 /// @brief Show a device-reported value of the profile's preset register (HeatingProfile::preset_fn).
125 /// @param profile The device's profile; one without a preset table changes nothing.
126 /// @param wire Value as the device reports it.
127 /// @return true when the preset changed; a value the profile does not map changes nothing.
128 bool apply_observed_preset_(const HeatingProfile &profile, uint8_t wire);
129
130 /// @brief Show one of the profile's presets, or none.
131 /// @param profile The device's profile.
132 /// @param preset_index Index into the profile's presets, or -1 for "no preset".
133 /// @return true when the shown preset changed.
134 bool show_preset_(const HeatingProfile &profile, int8_t preset_index);
135
136 /// @brief Send one preset value (or, on the register and level paths, one mode value) over the
137 /// profile's control path.
138 /// @param profile The device's profile.
139 /// @param fn Register to write (PRIVATE_REGISTERS only).
140 /// @param wire Register value, or the low byte of a level or alias.
141 /// @return true when the device accepted it.
142 bool write_(const HeatingProfile &profile, HeatingFunction fn, uint8_t wire);
143
144 /// @brief Send one preset over the profile's control path: a register or level write, or, for a
145 /// TEMPERATURE_EXECUTE heater, its alias as the main parameter with the entry's functional parameter.
146 /// @param profile The device's profile.
147 /// @param entry The preset entry.
148 /// @return true when the device accepted it.
149 bool write_preset_(const HeatingProfile &profile, const HeatingPresetEntry &entry);
150
151 /// @brief Encode a target temperature as a main parameter over the device's range (the fixed range
152 /// for a profile that has one).
153 /// @param profile The device's profile.
154 /// @param celsius The target.
155 /// @param[out] main_parameter The encoded value.
156 /// @return false when the temperature or the range cannot be encoded.
157 bool encode_target_(const HeatingProfile &profile, float celsius, uint16_t &main_parameter);
158
159 /// @brief Send an HVAC mode to a TEMPERATURE_EXECUTE heater: the mode entry's main parameter (the
160 /// target temperature for heat on a thermostat) with its functional parameter.
161 /// @param profile The device's profile.
162 /// @param entry The mode entry.
163 /// @return true when the device accepted it; false when it did not, or when heat needs a target
164 /// temperature that is not known yet.
165 bool write_temperature_mode_(const HeatingProfile &profile, const HeatingModeEntry &entry);
166
167 /// @brief Read one register back after an accepted write so the entity shows the heater's value.
168 /// @param fn Register that was just written.
170
171 /// @brief Apply the request's HVAC mode, if present. @return true if state was published.
172 bool apply_mode_(const HeatingProfile &profile, const climate::ClimateCall &call);
173 /// @brief Apply a custom preset, if present. @return true if published.
174 bool apply_custom_preset_(const HeatingProfile &profile, const climate::ClimateCall &call);
175 /// @brief Apply a standard preset, if present; "none" selects the profile's NONE entry where it has one.
176 /// @return true if published.
177 bool apply_preset_(const HeatingProfile &profile, const climate::ClimateCall &call);
178 /// @brief Send one preset and show it: the tail both preset paths share.
179 /// @details Refuses a preset the device only reports. Until a read has succeeded, a profile whose mode
180 /// is observed shows the preset through apply_observed_state_(); a temperature profile shows
181 /// the preset and, for a standard one, mode heat. A register write is read back.
182 /// @return true if the card changed and must be published.
183 bool commit_preset_(const HeatingProfile &profile, const HeatingPresetEntry &entry);
184 /// @brief Apply the target temperature, if present. @return true if published.
185 bool apply_target_temperature_(const HeatingProfile &profile, const climate::ClimateCall &call);
186
187 bool observed_{false}; ///< True once a reading has been folded in.
188 bool has_range_{false}; ///< YAML overrode the temperature encoding range.
189 float range_min_c_{HEATING_TEMPERATURE_RANGE_MIN_C}; ///< YAML `min_temperature:`.
190 float range_max_c_{HEATING_TEMPERATURE_RANGE_MAX_C}; ///< YAML `max_temperature:`.
191 bool mode_commanded_{false}; ///< A mode was commanded (matters where nothing reads the mode back).
192 float max_temperature_c_{HEATING_TEMP_MAX_C}; ///< Upper end of the target range (heater's own maximum once read).
193 std::vector<const char *> custom_presets_; ///< Custom preset names of the profile, registered in setup().
194};
195
196} // namespace home_io_control
197} // namespace esphome
Mixin holding the hub-device binding shared by all IO-Homecontrol entity platforms.
Climate entity for IO-Homecontrol heating devices.
bool write_temperature_mode_(const HeatingProfile &profile, const HeatingModeEntry &entry)
Send an HVAC mode to a TEMPERATURE_EXECUTE heater: the mode entry's main parameter (the target temper...
bool apply_custom_preset_(const HeatingProfile &profile, const climate::ClimateCall &call)
Apply a custom preset, if present.
bool show_preset_(const HeatingProfile &profile, int8_t preset_index)
Show one of the profile's presets, or none.
std::vector< const char * > custom_presets_
Custom preset names of the profile, registered in setup().
void on_device_update_(const std::string &id, const IoDevice &dev)
Inbound device-update hook: fold IoDevice::heating into the entity and publish when something it show...
const HeatingProfile * profile_() const
The device's heating profile: from the declared type, else from the type the hub learned.
bool commit_preset_(const HeatingProfile &profile, const HeatingPresetEntry &entry)
Send one preset and show it: the tail both preset paths share.
bool sync_action_(const HeatingProfile &profile, const HeatingReadings &readings, bool mode_known, bool &seen)
Fold the profile's action source (the regulator state, or the working rate) into the entity's action.
bool sync_from_readings_(const HeatingProfile &profile, const HeatingReadings &readings)
Copy the readings into the entity's fields.
void dump_config() override
Dump configuration to the log.
bool apply_target_temperature_(const HeatingProfile &profile, const climate::ClimateCall &call)
Apply the target temperature, if present.
bool apply_observed_state_(const HeatingProfile &profile, uint8_t wire)
Show a device-reported mode register value or heating level.
bool mode_commanded_
A mode was commanded (matters where nothing reads the mode back).
bool sync_temperatures_(const HeatingProfile &profile, const HeatingReadings &readings, float target_c, bool &seen)
Fold the temperatures (current, target, maximum) of the readings into the entity.
bool apply_observed_preset_(const HeatingProfile &profile, uint8_t wire)
Show a device-reported value of the profile's preset register (HeatingProfile::preset_fn).
void set_temperature_range(float min_c, float max_c)
Override the temperature encoding range (YAML min_temperature: / max_temperature:) of a heater that c...
bool apply_mode_(const HeatingProfile &profile, const climate::ClimateCall &call)
Apply the request's HVAC mode, if present.
bool has_range_
YAML overrode the temperature encoding range.
float range_max_c_
YAML max_temperature:.
float range_min_c_
YAML min_temperature:.
float get_setup_priority() const override
Get setup priority (DATA).
void confirm_write_(HeatingFunction fn)
Read one register back after an accepted write so the entity shows the heater's value.
climate::ClimateTraits traits() override
Traits from the device's heating profile: its HVAC modes and presets, current temperature and action ...
bool apply_preset_(const HeatingProfile &profile, const climate::ClimateCall &call)
Apply a standard preset, if present; "none" selects the profile's NONE entry where it has one.
bool write_(const HeatingProfile &profile, HeatingFunction fn, uint8_t wire)
Send one preset value (or, on the register and level paths, one mode value) over the profile's contro...
void control(const climate::ClimateCall &call) override
Apply a Home Assistant climate control request.
void setup() override
Register the entity with the shared hub and schedule the first poll of the heater.
float max_temperature_c_
Upper end of the target range (heater's own maximum once read).
bool encode_target_(const HeatingProfile &profile, float celsius, uint16_t &main_parameter)
Encode a target temperature as a main parameter over the device's range (the fixed range for a profil...
bool observed_
True once a reading has been folded in.
bool write_preset_(const HeatingProfile &profile, const HeatingPresetEntry &entry)
Send one preset over the profile's control path: a register or level write, or, for a TEMPERATURE_EXE...
HeatingFunction
Registers and functions of a 2W heater, one row of the descriptor table each.
constexpr float HEATING_TEMP_MAX_C
Highest setpoint this codec will encode.
Shared device-binding mixins for IO-Homecontrol entity platforms.
Pure codec for the private register protocol of 2W heaters (CMD_WRITE_PRIVATE 0x20).
Heating profiles: what a climate card offers for each kind of heater, and how.
One HVAC mode the card offers and the wire value that selects it.
What the hub has read from one heater so far.
Runtime state of a paired IO‑Homecontrol device.