Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
platform_climate.cpp
Go to the documentation of this file.
1/// @file platform_climate.cpp
2/// @brief Experimental climate entity for IO-Homecontrol heating devices (CMD_WRITE_PRIVATE 0x20).
3/// @ingroup hioc_platforms
4
5#include "platform_climate.h"
6
7#include "hub_internal.h"
8#include "esphome/core/log.h"
9
10#include <string>
11
12namespace esphome {
13namespace home_io_control {
14
15static const char *const TAG = "home_io_control.climate";
16
17namespace {
18
19/// @brief HA HVAC mode <-> IO heating mode. The single place this correspondence is defined.
20struct ClimateModeMapping {
21 climate::ClimateMode climate_mode;
22 HeatingMode heating_mode;
23};
24constexpr ClimateModeMapping CLIMATE_MODE_MAP[] = {
25 {climate::CLIMATE_MODE_OFF, HeatingMode::OFF},
26 {climate::CLIMATE_MODE_HEAT, HeatingMode::MANUAL},
27 {climate::CLIMATE_MODE_AUTO, HeatingMode::AUTO},
28};
29
30/// @brief HA preset <-> IO presence value (SET_PRESENCE: 1 = present/home, 0 = absent/away).
31struct ClimatePresetMapping {
32 climate::ClimatePreset preset;
33 float presence_value;
34};
35constexpr ClimatePresetMapping CLIMATE_PRESET_MAP[] = {
36 {climate::CLIMATE_PRESET_HOME, 1.0F},
37 {climate::CLIMATE_PRESET_AWAY, 0.0F},
38};
39
40/// @brief Custom preset name standing in for HeatingMode::PROG (there is no ClimateMode for a
41/// device-side schedule).
42constexpr const char *PROGRAM_PRESET_NAME = "Program";
43
44/// @brief UI target-temperature step. The wire carries 0.1 C (encode_heating_payload() encodes a
45/// 16-bit tenths-of-a-degree setpoint); 0.5 C is a UI-only choice for a sane thermostat card.
46constexpr float CLIMATE_TARGET_TEMP_STEP_C = 0.5F;
47
48} // namespace
49
51 // Device-type gating, the single predicate. A known non-climate type is a configuration
52 // mistake this entity can never drive, so fail it loudly rather than let every command be
53 // rejected downstream. An UNKNOWN type is left to pass through: discovery may still resolve it
54 // to a climate type, exactly as the other platforms tolerate unknown bindings. This runs
55 // BEFORE register_device_binding_() so a misconfigured binding never injects a device into the
56 // hub registry and never leaves a live callback on a failed entity.
58 ESP_LOGE(TAG,
59 "Device %s declares io_device_type '%s', which is not a climate device — disabling this climate entity",
60 this->device_id_.c_str(), device_type_name(this->device_type_));
61 this->mark_failed();
62 return;
63 }
64
65 // No initial status poll: CMD_WRITE_PRIVATE is write-only, so a heating device has nothing to
66 // read back and a status request would only draw a rejection warning at boot.
68 this, /*inverted=*/false,
69 [this](const std::string &id, const IoDevice &dev) { this->on_device_update_(id, dev); },
70 /*schedule_initial_poll=*/false);
71
72 // "Program" is a custom preset (registered on the entity, referenced by the traits) because IO's
73 // prog mode has no ClimateMode equivalent.
74 this->set_supported_custom_presets({PROGRAM_PRESET_NAME});
75}
76
77climate::ClimateTraits IOHomeClimate::traits() {
78 climate::ClimateTraits traits;
79 // No current-temperature stream: nothing in CMD_WRITE_PRIVATE or any decoded reply carries a
80 // measured room temperature, so the entity leaves current_temperature unset (NAN) and never
81 // advertises current-temperature support (the trait default).
82 for (const auto &mapping : CLIMATE_MODE_MAP)
83 traits.add_supported_mode(mapping.climate_mode);
84 for (const auto &mapping : CLIMATE_PRESET_MAP)
85 traits.add_supported_preset(mapping.preset);
86 // 7.0-28.0 C: the range encode_heating_payload() accepts (see HEATING_TEMP_MAX_C — the setpoint
87 // is a 16-bit LE tenths-of-a-degree field per the vendored Atlantic register map).
88 traits.set_visual_min_temperature(HEATING_TEMP_MIN_C);
89 traits.set_visual_max_temperature(HEATING_TEMP_MAX_C);
90 traits.set_visual_target_temperature_step(CLIMATE_TARGET_TEMP_STEP_C);
91 return traits;
92}
93
94void IOHomeClimate::control(const climate::ClimateCall &call) {
95 // Each field is independent: a failed send for one leaves that field's last-commanded value in
96 // place and does not block the others. Every helper runs; publish once if any field advanced.
97 const bool mode_changed = this->apply_mode_(call);
98 const bool custom_preset_changed = this->apply_custom_preset_(call);
99 const bool preset_changed = this->apply_preset_(call);
100 const bool temperature_changed = this->apply_target_temperature_(call);
101
102 // Published state is "last commanded, never confirmed" — no reply ever verifies it.
103 if (mode_changed || custom_preset_changed || preset_changed || temperature_changed)
104 this->publish_state();
105}
106
107bool IOHomeClimate::apply_mode_(const climate::ClimateCall &call) {
108 const auto &mode_opt = call.get_mode();
109 if (!mode_opt.has_value())
110 return false;
111 const climate::ClimateMode requested = *mode_opt;
112 for (const auto &mapping : CLIMATE_MODE_MAP) {
113 if (mapping.climate_mode != requested)
114 continue;
115 if (!this->parent_->send_heating_command(this->device_id_, HeatingFunction::SET_MODE,
116 static_cast<float>(mapping.heating_mode)))
117 return false;
118 this->mode = requested;
119 this->clear_custom_preset_(); // a real HVAC mode supersedes the "Program" custom preset
120 return true;
121 }
122 return false;
123}
124
125bool IOHomeClimate::apply_custom_preset_(const climate::ClimateCall &call) {
126 if (!call.has_custom_preset())
127 return false;
128 const std::string custom = std::string(call.get_custom_preset());
129 if (custom != PROGRAM_PRESET_NAME)
130 return false;
131 if (!this->parent_->send_heating_command(this->device_id_, HeatingFunction::SET_MODE,
132 static_cast<float>(HeatingMode::PROG)))
133 return false;
134 this->set_custom_preset_(call.get_custom_preset());
135 return true;
136}
137
138bool IOHomeClimate::apply_preset_(const climate::ClimateCall &call) {
139 const auto &preset_opt = call.get_preset();
140 if (!preset_opt.has_value())
141 return false;
142 const climate::ClimatePreset requested = *preset_opt;
143 for (const auto &mapping : CLIMATE_PRESET_MAP) {
144 if (mapping.preset != requested)
145 continue;
146 if (!this->parent_->send_heating_command(this->device_id_, HeatingFunction::SET_PRESENCE, mapping.presence_value))
147 return false;
148 this->preset = requested;
149 this->clear_custom_preset_(); // a HOME/AWAY preset supersedes the "Program" custom preset
150 return true;
151 }
152 return false;
153}
154
155bool IOHomeClimate::apply_target_temperature_(const climate::ClimateCall &call) {
156 const auto &target_opt = call.get_target_temperature();
157 if (!target_opt.has_value())
158 return false;
159 const float target = *target_opt;
160 if (!this->parent_->send_heating_command(this->device_id_, HeatingFunction::SET_TEMPERATURE, target))
161 return false;
162 this->target_temperature = target;
163 return true;
164}
165
166void IOHomeClimate::on_device_update_(const std::string & /*id*/, const IoDevice & /*dev*/) {
167 // Deliberately empty — a write-only heating device produces nothing this entity can publish.
168 // Last Contact / Active Issue reach the user through the companion diagnostic sensors.
169}
170
172 LOG_CLIMATE("", "IO-Homecontrol Climate", this);
173 ESP_LOGCONFIG(TAG, " Device ID: %s", this->device_id_.c_str());
174 ESP_LOGCONFIG(TAG, " Target temperature range: %.1f-%.1f C (documented max; write-only, never confirmed)",
176 // No poll-interval line: heating is write-only and never polls (status_poll_interval is
177 // rejected by climate.py).
178 ESP_LOGCONFIG(TAG, " Status: experimental, unvalidated on hardware");
179}
180
181} // namespace home_io_control
182} // namespace esphome
void register_device_binding_(Component *self, bool inverted, std::function< void(const std::string &, const IoDevice &)> on_update, bool schedule_initial_poll=true)
Perform the shared setup() registration ritual.
void on_device_update_(const std::string &id, const IoDevice &dev)
Inbound device-update hook.
bool apply_custom_preset_(const climate::ClimateCall &call)
Apply the "Program" custom preset via SET_MODE(prog), if present.
void dump_config() override
Dump configuration to the log.
bool apply_target_temperature_(const climate::ClimateCall &call)
Apply the target temperature via SET_TEMPERATURE, if present.
bool apply_preset_(const climate::ClimateCall &call)
Apply a HOME/AWAY preset via SET_PRESENCE, if present.
climate::ClimateTraits traits() override
Static traits: modes OFF/HEAT/AUTO, presets HOME/AWAY, temperature range HEATING_TEMP_MIN_C....
void control(const climate::ClimateCall &call) override
Apply a Home Assistant climate control request.
void setup() override
Register the entity with the shared hub (no status poll — heating has no readback).
bool apply_mode_(const climate::ClimateCall &call)
Apply the request's HVAC mode via SET_MODE, if present.
Internal helpers shared by the hub implementation .cpp files.
@ UNKNOWN
Unknown/unspecified device.
@ SET_PRESENCE
Presence / absence (iohcCozyDevice2W.cpp:195).
@ SET_TEMPERATURE
Setpoint in degrees Celsius (iohcCozyDevice2W.cpp:125).
@ SET_MODE
Operating mode (iohcCozyDevice2W.cpp:155).
static constexpr const char * TAG
bool device_supports_climate_control(DeviceType type)
Does this device type support 2W climate/heating control (CMD_WRITE_PRIVATE 0x20)?
constexpr float HEATING_TEMP_MAX_C
Highest setpoint this codec will encode.
const char * device_type_name(DeviceType type)
Convert a DeviceType to a lowercase string identifier.
constexpr float HEATING_TEMP_MIN_C
Lowest setpoint this codec will encode.
HeatingMode
Operating modes for HeatingFunction::SET_MODE.
@ 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).
Experimental climate entity for IO-Homecontrol heating devices (CMD_WRITE_PRIVATE 0x20).
Runtime state of a paired IO‑Homecontrol device.