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