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. A preset that no register reports back (presence) stays "last commanded". The
19/// entity deliberately does NOT use the OptimisticState overlay (ADR 0030) — there is never a
20/// prediction to keep apart from an observation (ADR 0054, ADR 0055).
21///
22/// The register path is confirmed on an Atlantic AGILIA IO radiator: setpoint and mode writes, their
23/// read-back, and the poll's room temperature and regulator readings
24/// ([issue #165](https://github.com/laberning/home_io_control/issues/165), corpus
25/// tests/corpus/captures/exchange/atlantic_agilia_exchange_status_read.yaml).
26///
27/// @todo The heating-level and temperature EXECUTE paths, and the presence write behind the Home / Away
28/// presets, are unconfirmed on hardware; confirm with a capture from a plain heater, a heating
29/// temperature interface or a heat pump.
30///
31/// @warning The EXECUTE-commanded profiles are unvalidated on hardware. See
32/// docs/configuration/climate.md.
33
34#include "esphome/components/climate/climate.h"
35#include "esphome/core/component.h"
37#include "proto_heating.h"
39
40#include <vector>
41
42namespace esphome {
43namespace home_io_control {
44
45/// @brief Climate entity for IO-Homecontrol heating devices.
46/// @ingroup hioc_platforms
47///
48/// The device-binding setters and setup() registration ritual come from DeviceBoundEntity; the
49/// profile-to-ESPHome mapping and the publish-from-readings discipline live here.
50class IOHomeClimate : public climate::Climate, public Component, public DeviceBoundEntity {
51 public:
52 /// @brief Register the entity with the shared hub and schedule the first poll of the heater.
53 void setup() override;
54 /// @brief Dump configuration to the log.
55 void dump_config() override;
56 /// @brief Get setup priority (DATA).
57 /// @return setup_priority::DATA.
58 [[nodiscard]] float get_setup_priority() const override { return setup_priority::DATA; }
59
60 /// @brief Traits from the device's heating profile: its HVAC modes and presets, current temperature
61 /// and action where the profile has them, and a target range from HEATING_TEMP_MIN_C to the
62 /// heater's own maximum setpoint (HEATING_TEMP_MAX_C until it has been read) at a 0.5 C step. A
63 /// device whose type is not known yet gets the off mode only.
64 climate::ClimateTraits traits() override;
65
66 /// @brief Apply a Home Assistant climate control request.
67 ///
68 /// Each requested mode, preset and setpoint is looked up in the profile and sent over its control
69 /// path. After an accepted register write the register is read back; what the heater answers is
70 /// what the entity then shows. Only while no read has succeeded yet does an accepted write publish
71 /// the commanded value. A failed send leaves the entity state untouched.
72 /// @param call The requested attribute changes.
73 void control(const climate::ClimateCall &call) override;
74
75 /// @brief Override the temperature encoding range (YAML `min_temperature:` / `max_temperature:`) of a
76 /// heater that carries a temperature in its main parameter; applied to the device in setup().
77 /// @param min_c Lower end of the range in degrees Celsius.
78 /// @param max_c Upper end of the range.
79 void set_temperature_range(float min_c, float max_c) {
80 this->range_min_c_ = min_c;
81 this->range_max_c_ = max_c;
82 this->has_range_ = true;
83 }
84
85 protected:
86 /// @brief The device's heating profile: from the declared type, else from the type the hub learned.
87 /// @return The profile, or nullptr while the type is unknown.
88 [[nodiscard]] const HeatingProfile *profile_() const;
89
90 /// @brief Inbound device-update hook: fold IoDevice::heating into the entity and publish when
91 /// something it shows changed. Ignores updates for other devices.
92 void on_device_update_(const std::string &id, const IoDevice &dev);
93
94 /// @brief Copy the readings into the entity's fields.
95 /// @param profile The device's profile.
96 /// @param readings What the hub has read from the heater.
97 /// @return true when a published field (including the traits' maximum) changed.
98 bool sync_from_readings_(const HeatingProfile &profile, const HeatingReadings &readings);
99
100 /// @brief Fold the temperatures (current, setpoint, maximum) of the readings into the entity.
101 /// @param profile The device's profile.
102 /// @param readings What the hub has read from the heater.
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, bool &seen);
106
107 /// @brief Fold the regulator state into the entity's action.
108 /// @param profile The device's profile.
109 /// @param readings What the hub has read from the heater.
110 /// @param mode_known True when the mode or level has been read (an off heater is then not idle).
111 /// @param[in,out] seen Set when a reading was present.
112 /// @return true when the action changed.
113 bool sync_action_(const HeatingProfile &profile, const HeatingReadings &readings, bool mode_known, bool &seen);
114
115 /// @brief Show a device-reported mode register value or heating level.
116 /// @param profile The device's profile.
117 /// @param wire Value as the device reports it.
118 /// @return true when the mode or a preset changed; a value the profile does not map changes nothing.
119 bool apply_observed_state_(const HeatingProfile &profile, uint8_t wire);
120
121 /// @brief Send one preset value (or, on the register and level paths, one mode value) over the
122 /// profile's control path.
123 /// @param profile The device's profile.
124 /// @param fn Register to write (PRIVATE_REGISTERS only).
125 /// @param wire Register value, or the low byte of a level or alias.
126 /// @return true when the device accepted it.
127 bool write_(const HeatingProfile &profile, HeatingFunction fn, uint8_t wire);
128
129 /// @brief Send one preset over the profile's control path: a register or level write, or, for a
130 /// TEMPERATURE_EXECUTE heater, its alias as the main parameter with the entry's functional parameter.
131 /// @param profile The device's profile.
132 /// @param entry The preset entry.
133 /// @return true when the device accepted it.
134 bool write_preset_(const HeatingProfile &profile, const HeatingPresetEntry &entry);
135
136 /// @brief Encode a target temperature as a main parameter over the device's range (the fixed range
137 /// for a profile that has one).
138 /// @param profile The device's profile.
139 /// @param celsius The target.
140 /// @param[out] main_parameter The encoded value.
141 /// @return false when the temperature or the range cannot be encoded.
142 bool encode_target_(const HeatingProfile &profile, float celsius, uint16_t &main_parameter);
143
144 /// @brief Send an HVAC mode to a TEMPERATURE_EXECUTE heater: the mode entry's main parameter (the
145 /// target temperature for heat on a thermostat) with its functional parameter.
146 /// @param profile The device's profile.
147 /// @param entry The mode entry.
148 /// @return true when the device accepted it; false when it did not, or when heat needs a target
149 /// temperature that is not known yet.
150 bool write_temperature_mode_(const HeatingProfile &profile, const HeatingModeEntry &entry);
151
152 /// @brief Read one register back after an accepted write so the entity shows the heater's value.
153 /// @param fn Register that was just written.
155
156 /// @brief Apply the request's HVAC mode, if present. @return true if state was published.
157 bool apply_mode_(const HeatingProfile &profile, const climate::ClimateCall &call);
158 /// @brief Apply a custom preset, if present. @return true if published.
159 bool apply_custom_preset_(const HeatingProfile &profile, const climate::ClimateCall &call);
160 /// @brief Apply a standard preset, if present. @return true if published.
161 bool apply_preset_(const HeatingProfile &profile, const climate::ClimateCall &call);
162 /// @brief Apply the target temperature, if present. @return true if published.
163 bool apply_target_temperature_(const HeatingProfile &profile, const climate::ClimateCall &call);
164
165 bool observed_{false}; ///< True once a reading has been folded in.
166 bool has_range_{false}; ///< YAML overrode the temperature encoding range.
167 float range_min_c_{HEATING_TEMPERATURE_RANGE_MIN_C}; ///< YAML `min_temperature:`.
168 float range_max_c_{HEATING_TEMPERATURE_RANGE_MAX_C}; ///< YAML `max_temperature:`.
169 bool mode_commanded_{false}; ///< A mode was commanded (matters where nothing reads the mode back).
170 float max_temperature_c_{HEATING_TEMP_MAX_C}; ///< Upper end of the target range (heater's own maximum once read).
171 std::vector<const char *> custom_presets_; ///< Custom preset names of the profile, registered in setup().
172};
173
174} // namespace home_io_control
175} // 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.
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 sync_action_(const HeatingProfile &profile, const HeatingReadings &readings, bool mode_known, bool &seen)
Fold the regulator state 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 sync_temperatures_(const HeatingProfile &profile, const HeatingReadings &readings, bool &seen)
Fold the temperatures (current, setpoint, maximum) of the readings into the entity.
bool mode_commanded_
A mode was commanded (matters where nothing reads the mode back).
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.
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.