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 heaters, driven by a heating profile.
3/// @ingroup hioc_platforms
4
5#include "platform_climate.h"
6
9
10#include "esphome/core/log.h"
11
12#include <algorithm>
13#include <cinttypes>
14#include <cmath>
15#include <string>
16
17namespace esphome {
18namespace home_io_control {
19
20static const char *const TAG = "home_io_control.climate";
21
22namespace {
23
24/// @brief Profile HVAC mode <-> ESPHome climate mode. The single place this correspondence is defined.
25struct ClimateModeMapping {
26 HeatingHvacMode hvac;
27 climate::ClimateMode climate_mode;
28};
29constexpr ClimateModeMapping CLIMATE_MODE_MAP[] = {
30 {HeatingHvacMode::OFF, climate::CLIMATE_MODE_OFF},
31 {HeatingHvacMode::HEAT, climate::CLIMATE_MODE_HEAT},
32 {HeatingHvacMode::AUTO, climate::CLIMATE_MODE_AUTO},
33};
34
35/// @brief Profile standard preset <-> ESPHome preset. Custom presets are named by the profile.
36struct ClimatePresetMapping {
38 climate::ClimatePreset preset;
39};
40constexpr ClimatePresetMapping CLIMATE_PRESET_MAP[] = {
41 {HeatingPresetId::HOME, climate::CLIMATE_PRESET_HOME}, {HeatingPresetId::AWAY, climate::CLIMATE_PRESET_AWAY},
42 {HeatingPresetId::COMFORT, climate::CLIMATE_PRESET_COMFORT}, {HeatingPresetId::ECO, climate::CLIMATE_PRESET_ECO},
43 {HeatingPresetId::BOOST, climate::CLIMATE_PRESET_BOOST},
44};
45
46/// @brief UI target-temperature step. The wire carries 0.1 C (encode_heating_write() encodes a
47/// 16-bit tenths-of-a-degree setpoint); 0.5 C is a UI-only choice for a sane thermostat card.
48constexpr float CLIMATE_TARGET_TEMP_STEP_C = 0.5F;
49
50climate::ClimateMode to_climate_mode(HeatingHvacMode hvac) {
51 for (const auto &mapping : CLIMATE_MODE_MAP) {
52 if (mapping.hvac == hvac)
53 return mapping.climate_mode;
54 }
55 return climate::CLIMATE_MODE_OFF;
56}
57
58/// @return The ESPHome preset of a standard profile preset; CLIMATE_PRESET_NONE for a custom one.
59climate::ClimatePreset to_climate_preset(HeatingPresetId id) {
60 for (const auto &mapping : CLIMATE_PRESET_MAP) {
61 if (mapping.id == id)
62 return mapping.preset;
63 }
64 return climate::CLIMATE_PRESET_NONE;
65}
66
67/// @return true when the profile has a standard preset that a device reading decides (not presence).
68bool has_observed_standard_preset(const HeatingProfile &profile) {
69 for (size_t i = 0; i < profile.preset_count; i++) {
70 if (profile.presets[i].id != HeatingPresetId::CUSTOM && profile.presets[i].observed)
71 return true;
72 }
73 return false;
74}
75
76} // namespace
77
80 return find_heating_profile(this->device_type_, this->subtype_);
81 // Declared without a type: use the one the hub has learned from the device since.
82 const IoDevice *dev = this->parent_ != nullptr ? this->parent_->get_device(this->device_id_) : nullptr;
83 return dev != nullptr ? find_heating_profile(dev->type, dev->subtype) : nullptr;
84}
85
87 // Device gating, the single predicate. A known device this entity cannot drive (a non-climate
88 // type, or a heater kind without a profile) is a configuration mistake, so fail it loudly rather
89 // than let every command be rejected downstream. An UNKNOWN type is left to pass through: discovery
90 // may still resolve it to a climate type, exactly as the other platforms tolerate unknown bindings.
91 // This runs BEFORE register_device_binding_() so a misconfigured binding never injects a device
92 // into the hub registry and never leaves a live callback on a failed entity.
93 if (this->device_type_ != DeviceType::UNKNOWN &&
95 ESP_LOGE(TAG,
96 "Device %s is declared as %s, which this climate entity cannot drive yet (it drives an electrical "
97 "heater: io_device_type: electrical_heater, io_subtype: 0 or 1) — disabling it",
98 this->device_id_.c_str(), format_device_kind(this->device_type_, this->subtype_).c_str());
99 this->mark_failed();
100 return;
101 }
102
103 // The initial poll reads the heater (IOHomeControlComponent::request_device_status()), so the card
104 // shows measured values from the first minute instead of nothing.
105 this->register_device_binding_(this, /*inverted=*/false, [this](const std::string &id, const IoDevice &dev) {
106 this->on_device_update_(id, dev);
107 });
108
109 if (this->has_range_)
111
112 // Custom presets are registered on the entity and referenced by the traits; IO's program mode,
113 // the comfort offsets and the secured level have no standard preset.
114 if (const HeatingProfile *profile = this->profile_(); profile != nullptr) {
115 if (profile->features.target_temperature)
116 this->max_temperature_c_ = profile->visual_max_c;
117 for (size_t i = 0; i < profile->preset_count; i++) {
118 if (profile->presets[i].id == HeatingPresetId::CUSTOM)
119 this->custom_presets_.push_back(profile->presets[i].custom_name);
120 }
121 this->set_supported_custom_presets(this->custom_presets_);
122 }
123}
124
125climate::ClimateTraits IOHomeClimate::traits() {
126 climate::ClimateTraits traits;
127 const HeatingProfile *profile = this->profile_();
128 if (profile == nullptr) {
129 // Type not known yet: the one mode every heater has.
130 traits.add_supported_mode(climate::CLIMATE_MODE_OFF);
131 return traits;
132 }
133
134 // Both are filled only from a polled reply (sync_from_readings_()): a card that advertises them
135 // shows an empty value until the first poll lands, which is the honest state.
136 if (profile->features.current_temperature)
137 traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
138 if (profile->features.action)
139 traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
140 for (size_t i = 0; i < profile->mode_count; i++)
141 traits.add_supported_mode(to_climate_mode(profile->modes[i].hvac));
142 for (size_t i = 0; i < profile->preset_count; i++) {
143 if (profile->presets[i].id != HeatingPresetId::CUSTOM)
144 traits.add_supported_preset(to_climate_preset(profile->presets[i].id));
145 }
146 // HEATING_TEMP_MIN_C up to the heater's own maximum setpoint once read, never past what
147 // encode_heating_write() accepts (HEATING_TEMP_MAX_C).
148 if (profile->features.target_temperature) {
149 traits.set_visual_min_temperature(profile->visual_min_c);
150 traits.set_visual_max_temperature(this->max_temperature_c_);
151 traits.set_visual_target_temperature_step(CLIMATE_TARGET_TEMP_STEP_C);
152 }
153 return traits;
154}
155
156void IOHomeClimate::control(const climate::ClimateCall &call) {
157 const HeatingProfile *profile = this->profile_();
158 if (profile == nullptr) {
159 ESP_LOGW(TAG, "Device %s: type not known yet, ignoring the request", this->device_id_.c_str());
160 return;
161 }
162 // Each field is independent: a failed send for one leaves that field's state in place and does not
163 // block the others. Every helper runs; publish once if any field advanced without a read-back.
164 const bool mode_changed = this->apply_mode_(*profile, call);
165 const bool custom_preset_changed = this->apply_custom_preset_(*profile, call);
166 const bool preset_changed = this->apply_preset_(*profile, call);
167 const bool temperature_changed = this->apply_target_temperature_(*profile, call);
168
169 // A read-back publishes through on_device_update_(); this publishes what a write advanced when no
170 // read-back has confirmed anything yet, and the presence preset, which has no register to read.
171 if (mode_changed || custom_preset_changed || preset_changed || temperature_changed)
172 this->publish_state();
173}
174
175bool IOHomeClimate::write_(const HeatingProfile &profile, HeatingFunction fn, uint8_t wire) {
176 switch (profile.path) {
178 return this->parent_->set_heating_level(this->device_id_, wire);
180 return this->parent_->send_heating_command(this->device_id_, fn, static_cast<float>(wire));
182 break; // its commands carry a main parameter and a functional parameter: write_preset_() and friends
183 }
184 return false;
185}
186
189 return this->write_(profile, entry.write_fn, entry.wire);
190 // A setpoint alias is a main parameter, with the functional parameter the profile names for it.
191 return this->parent_->set_heating_temperature(
192 this->device_id_, heating_prefixed_main_parameter(profile.mp_prefix, entry.wire), entry.fp);
193}
194
195bool IOHomeClimate::encode_target_(const HeatingProfile &profile, float celsius, uint16_t &main_parameter) {
196 const IoDevice *dev = this->parent_->get_device(this->device_id_);
197 if (dev == nullptr)
198 return false;
199 // A profile with a fixed range never takes the YAML range, whatever the record holds.
200 const float min_c = profile.fixed_range ? HEATING_TEMPERATURE_RANGE_MIN_C : dev->temperature_range_min_c;
201 const float max_c = profile.fixed_range ? HEATING_TEMPERATURE_RANGE_MAX_C : dev->temperature_range_max_c;
202 return encode_temperature_main_parameter(celsius, min_c, max_c, main_parameter);
203}
204
206 uint16_t main_parameter = entry.main_parameter;
207 if (main_parameter == HEATING_MP_FROM_TARGET) {
208 // Heat (manual) is the current target temperature with the manual flag: the target the card shows,
209 // else the one the device last reported.
210 const IoDevice *dev = this->parent_->get_device(this->device_id_);
211 float target = this->target_temperature;
212 if (std::isnan(target) && dev != nullptr)
213 target = dev->heating.setpoint_c;
214 if (std::isnan(target) || !this->encode_target_(profile, target, main_parameter)) {
215 ESP_LOGW(TAG, "Device %s: no target temperature known yet, set one before selecting heat",
216 this->device_id_.c_str());
217 return false;
218 }
219 }
220 return this->parent_->set_heating_temperature(this->device_id_, main_parameter, entry.fp);
221}
222
224 // A failed read is already logged by the hub, and the poll will find the register again; the
225 // entity keeps whatever it showed. A level write needs no call here: the hub arms a settle poll.
226 this->parent_->read_heating_register(this->device_id_, fn, nullptr, EXCHANGE_RETRY_COUNT);
227}
228
229bool IOHomeClimate::apply_mode_(const HeatingProfile &profile, const climate::ClimateCall &call) {
230 const auto &mode_opt = call.get_mode();
231 if (!mode_opt.has_value())
232 return false;
233 const climate::ClimateMode requested = *mode_opt;
234 for (size_t i = 0; i < profile.mode_count; i++) {
235 const HeatingModeEntry &entry = profile.modes[i];
236 if (to_climate_mode(entry.hvac) != requested)
237 continue;
238 const bool sent = profile.path == HeatingControlPath::TEMPERATURE_EXECUTE
239 ? this->write_temperature_mode_(profile, entry)
240 : this->write_(profile, HeatingFunction::SET_MODE, entry.wire);
241 if (!sent)
242 return false;
243 // Until a read has succeeded there is nothing observed to show: the commanded mode stands in. A mode
244 // that nothing reads back stays the last command.
245 const bool commanded = !this->observed_ || !profile.features.mode_observed;
246 this->mode_commanded_ = true;
247 if (commanded) {
248 this->mode = requested;
249 // A real HVAC mode supersedes a preset on the register and level paths; a temperature heater's
250 // preset follows its status reply instead.
252 this->clear_custom_preset_();
253 if (has_observed_standard_preset(profile))
254 this->preset.reset();
255 }
256 }
259 return commanded && !this->observed_;
260 }
261 return commanded;
262 }
263 return false;
264}
265
266bool IOHomeClimate::apply_custom_preset_(const HeatingProfile &profile, const climate::ClimateCall &call) {
267 if (!call.has_custom_preset())
268 return false;
269 const std::string custom = std::string(call.get_custom_preset());
270 for (size_t i = 0; i < profile.preset_count; i++) {
271 const HeatingPresetEntry &entry = profile.presets[i];
272 if (entry.id != HeatingPresetId::CUSTOM || custom != entry.custom_name)
273 continue;
274 if (!entry.commandable) {
275 ESP_LOGW(TAG, "Device %s: preset '%s' is reported by the device and cannot be set", this->device_id_.c_str(),
276 entry.custom_name);
277 return false;
278 }
279 if (!this->write_preset_(profile, entry))
280 return false;
281 const bool commanded = !this->observed_ || !entry.observed;
282 if (commanded)
283 this->set_custom_preset_(entry.custom_name);
284 if (profile.path == HeatingControlPath::PRIVATE_REGISTERS && entry.observed) {
285 this->confirm_write_(entry.write_fn);
286 return commanded && !this->observed_;
287 }
288 return commanded;
289 }
290 return false;
291}
292
293bool IOHomeClimate::apply_preset_(const HeatingProfile &profile, const climate::ClimateCall &call) {
294 const auto &preset_opt = call.get_preset();
295 if (!preset_opt.has_value())
296 return false;
297 const climate::ClimatePreset requested = *preset_opt;
298 for (size_t i = 0; i < profile.preset_count; i++) {
299 const HeatingPresetEntry &entry = profile.presets[i];
300 if (entry.id == HeatingPresetId::CUSTOM || to_climate_preset(entry.id) != requested)
301 continue;
302 if (!entry.commandable || !this->write_preset_(profile, entry))
303 return false;
304 // A preset no register reports (presence) stays "last commanded" for good.
305 const bool commanded = !this->observed_ || !entry.observed;
306 if (commanded) {
307 this->preset = requested;
308 this->clear_custom_preset_(); // a standard preset supersedes a custom one
309 if (profile.path != HeatingControlPath::PRIVATE_REGISTERS && !this->observed_)
310 this->mode = climate::CLIMATE_MODE_HEAT; // every level or alias preset heats
311 }
312 if (profile.path == HeatingControlPath::PRIVATE_REGISTERS && entry.observed) {
313 this->confirm_write_(entry.write_fn);
314 return commanded && !this->observed_;
315 }
316 return commanded;
317 }
318 return false;
319}
320
321bool IOHomeClimate::apply_target_temperature_(const HeatingProfile &profile, const climate::ClimateCall &call) {
322 const auto &target_opt = call.get_target_temperature();
323 if (!target_opt.has_value())
324 return false;
325 if (!profile.features.target_temperature) {
326 ESP_LOGW(TAG, "Device %s has no adjustable setpoint; the request is ignored", this->device_id_.c_str());
327 return false;
328 }
329 const float target = *target_opt;
331 // The temperature travels in the main parameter, encoded over the device's range; the settle poll
332 // reads it back.
333 uint16_t main_parameter = 0;
334 if (!this->encode_target_(profile, target, main_parameter) ||
335 !this->parent_->set_heating_temperature(this->device_id_, main_parameter, profile.setpoint_fp))
336 return false;
337 const bool commanded = !this->observed_;
338 if (commanded) {
339 this->target_temperature = target;
340 this->clear_custom_preset_();
341 this->preset.reset();
342 }
343 return commanded;
344 }
345 if (!this->parent_->send_heating_command(this->device_id_, HeatingFunction::SET_TEMPERATURE, target))
346 return false;
347 const bool commanded = !this->observed_;
348 if (commanded)
349 this->target_temperature = target;
351 return commanded && !this->observed_;
352}
353
354bool IOHomeClimate::sync_temperatures_(const HeatingProfile &profile, const HeatingReadings &readings, bool &seen) {
355 bool changed = false;
356 // NaN means "never read" (or a sensor fault): two NaNs are the same state, not a change.
357 const auto differs = [](float a, float b) { return !(std::isnan(a) && std::isnan(b)) && a != b; };
358
359 if (profile.features.current_temperature) {
360 if (differs(this->current_temperature, readings.room_temperature_c)) {
361 this->current_temperature = readings.room_temperature_c;
362 changed = true;
363 }
364 seen = seen || !std::isnan(readings.room_temperature_c);
365 }
366
367 if (profile.features.target_temperature && !std::isnan(readings.setpoint_c)) {
368 seen = true;
369 if (differs(this->target_temperature, readings.setpoint_c)) {
370 this->target_temperature = readings.setpoint_c;
371 changed = true;
372 }
373 }
374
375 // The heater's own ceiling, but never past what a write can encode.
376 if (profile.features.target_temperature && !std::isnan(readings.max_setpoint_c) &&
378 seen = true;
379 const float max_c = std::min(readings.max_setpoint_c, profile.visual_max_c);
380 if (max_c != this->max_temperature_c_) {
381 this->max_temperature_c_ = max_c;
382 changed = true;
383 }
384 }
385 return changed;
386}
387
388bool IOHomeClimate::sync_action_(const HeatingProfile &profile, const HeatingReadings &readings, bool mode_known,
389 bool &seen) {
390 if (!profile.features.action)
391 return false;
392 // The action follows the mode: an off heater is not "idle". Until the regulator has been read only
393 // the off case is known.
394 climate::ClimateAction action = this->action;
395 if (this->mode == climate::CLIMATE_MODE_OFF && mode_known) {
396 action = climate::CLIMATE_ACTION_OFF;
397 } else if (readings.regulator >= 0) {
398 seen = true;
399 action = readings.regulator == static_cast<int16_t>(HeatingRegulatorState::HEATING)
400 ? climate::CLIMATE_ACTION_HEATING
401 : climate::CLIMATE_ACTION_IDLE;
402 }
403 if (action == this->action)
404 return false;
405 this->action = action;
406 return true;
407}
408
410 bool seen = false;
411 bool changed = this->sync_temperatures_(profile, readings, seen);
412
413 // The mode register, or the heating level, whichever this profile's device reports.
414 const int16_t wire = profile.path == HeatingControlPath::PRIVATE_REGISTERS ? readings.mode : readings.level;
415 if (wire >= 0) {
416 seen = true;
417 changed = this->apply_observed_state_(profile, static_cast<uint8_t>(wire)) || changed;
418 } else if (profile.path == HeatingControlPath::TEMPERATURE_EXECUTE && !std::isnan(readings.setpoint_c)) {
419 // The device reports a plain temperature, so it is on no alias preset.
420 if (this->has_custom_preset()) {
421 this->clear_custom_preset_();
422 changed = true;
423 }
424 if (this->preset.has_value()) {
425 this->preset.reset();
426 changed = true;
427 }
428 }
429 changed = this->sync_action_(profile, readings, wire >= 0, seen) || changed;
430
431 // Where nothing reads the mode back, a device that reports a setpoint or an alias is evidently on:
432 // show heat until a mode is commanded, instead of the entity's default "off".
433 if (!profile.features.mode_observed && !this->mode_commanded_ && seen && this->mode == climate::CLIMATE_MODE_OFF) {
434 this->mode = climate::CLIMATE_MODE_HEAT;
435 changed = true;
436 }
437
438 this->observed_ = this->observed_ || seen;
439 return changed;
440}
441
442bool IOHomeClimate::apply_observed_state_(const HeatingProfile &profile, uint8_t wire) {
443 const HeatingObservedState *observed = find_observed_state(profile, wire);
444 if (observed == nullptr)
445 return false; // unknown value: leave the card as it was
446
447 // A mode nothing reads back stays the last command; the table then only names the preset.
448 bool changed = false;
449 if (profile.features.mode_observed) {
450 const climate::ClimateMode mode = to_climate_mode(observed->hvac);
451 changed = mode != this->mode;
452 this->mode = mode;
453 }
454
455 const HeatingPresetEntry *shown = observed->preset >= 0 ? &profile.presets[observed->preset] : nullptr;
456 if (shown != nullptr && shown->id == HeatingPresetId::CUSTOM) {
457 if (!this->has_custom_preset() || this->get_custom_preset() != shown->custom_name) {
458 this->set_custom_preset_(shown->custom_name);
459 changed = true;
460 }
461 } else {
462 if (this->has_custom_preset()) {
463 this->clear_custom_preset_();
464 changed = true;
465 }
466 if (shown != nullptr) {
467 const climate::ClimatePreset preset = to_climate_preset(shown->id);
468 if (!this->preset.has_value() || *this->preset != preset) {
469 this->preset = preset;
470 changed = true;
471 }
472 } else if (has_observed_standard_preset(profile) && this->preset.has_value()) {
473 // The device reports it is on no preset; presence presets are last-commanded and stay.
474 this->preset.reset();
475 changed = true;
476 }
477 }
478 return changed;
479}
480
481void IOHomeClimate::on_device_update_(const std::string &id, const IoDevice &dev) {
482 if (id != this->device_id_)
483 return;
484 const HeatingProfile *profile = find_heating_profile(dev.type, dev.subtype);
485 if (profile == nullptr)
486 return;
487 if (this->sync_from_readings_(*profile, dev.heating))
488 this->publish_state();
489}
490
492 LOG_CLIMATE("", "IO-Homecontrol Climate", this);
493 ESP_LOGCONFIG(TAG, " Device ID: %s", this->device_id_.c_str());
494 ESP_LOGCONFIG(TAG, " Target temperature range: %.1f-%.1f C (maximum from the heater once read)", HEATING_TEMP_MIN_C,
495 this->max_temperature_c_);
496 ESP_LOGCONFIG(
497 TAG, " Status Poll Interval: %" PRIu32 " ms",
498 this->status_poll_interval_ms_ != 0 ? this->status_poll_interval_ms_ : HEATING_POLL_DEFAULT_INTERVAL_MS);
499 // Only the register profile has run on hardware (an Atlantic AGILIA IO radiator, issue #165).
500 const HeatingProfile *profile = this->profile_();
501 const bool confirmed = profile != nullptr && profile->path == HeatingControlPath::PRIVATE_REGISTERS;
502 ESP_LOGCONFIG(TAG, " Status: %s",
503 confirmed ? "confirmed on hardware (setpoint, mode, read-back); Home/Away presets untested"
504 : "experimental, unvalidated on hardware");
505}
506
507} // namespace home_io_control
508} // namespace esphome
void register_device_binding_(Component *self, bool inverted, std::function< void(const std::string &, const IoDevice &)> on_update)
Perform the shared setup() registration ritual.
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).
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:.
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...
virtual bool send_heating_command(const std::string &device_id, HeatingFunction fn, float value)
Run one heating function on the heater's private register protocol (CMD_WRITE_PRIVATE 0x20): a write ...
virtual bool set_heating_temperature(const std::string &device_id, uint16_t main_parameter, const HeatingFp &fp)
Command a heater that carries a temperature or an alias in its main parameter (the heating temperatur...
virtual IoDevice * get_device(const std::string &device_id)
Retrieve a device by ID; returns nullptr if not found.
Definition hub_core.cpp:350
virtual bool read_heating_register(const std::string &device_id, HeatingFunction fn, float *value_out, uint8_t max_tries)
Read one register of a heater and fold the decoded value into IoDevice::heating.
virtual bool set_heating_level(const std::string &device_id, uint8_t level)
Set the heating level of a heater that is commanded with levels (the plain electrical heater).
virtual void set_device_temperature_range(const std::string &device_id, float min_c, float max_c)
Set the temperature encoding range of a heater that carries a temperature in its main parameter (YAML...
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.
HeatingFunction
Registers and functions of a 2W heater, one row of the descriptor table each.
@ 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).
static constexpr const char * TAG
HeatingPresetId
Presets a profile can offer. CUSTOM entries carry their own name.
@ CUSTOM
Named by HeatingPresetEntry::custom_name.
@ AWAY
Nobody is in the room (the frost-protection level on a level heater).
@ UNKNOWN
Unknown/unspecified device.
@ 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.
bool device_supports_climate_control(DeviceType type, uint8_t subtype)
Can a climate entity drive this device?
constexpr float HEATING_TEMP_MIN_C
Lowest setpoint this codec will encode.
std::string format_device_kind(DeviceType type, uint8_t subtype)
Human-readable description of a (type, subtype) for logs and reports.
constexpr uint16_t heating_prefixed_main_parameter(uint8_t prefix, uint8_t low)
Main parameter of a prefixed value (a level or an alias).
HeatingHvacMode
HVAC modes a profile can offer.
static const char *const TAG
Experimental climate entity for IO-Homecontrol heaters, driven by a heating profile.
The IO node-type and subtype table: names, entity class, platform and behaviour flags.
Per-device poll scheduling, failure backoff, and follow-up-poll state machine.
One HVAC mode the card offers and the wire value that selects it.
HeatingFp fp
TEMPERATURE_EXECUTE: the functional parameter the mode sends.
uint16_t main_parameter
TEMPERATURE_EXECUTE: the main parameter the mode sends, or HEATING_MP_FROM_TARGET.
HeatingHvacMode hvac
Mode shown on the card.
uint8_t wire
PRIVATE_REGISTERS: mode register value.
What a device-reported wire value shows on the card.
int8_t preset
Index into the profile's presets shown with it, or -1 for none.
const char * custom_name
Name of a CUSTOM preset; nullptr otherwise.
HeatingFp fp
TEMPERATURE_EXECUTE: the functional parameter sent with the alias.
uint8_t wire
Value written: register value, or the low byte of a level or alias.
bool observed
False when no register reports it back (presence): the card then shows the last command.
HeatingPresetId id
Standard preset, or CUSTOM.
HeatingFunction write_fn
PRIVATE_REGISTERS: register the preset writes. Unused for levels.
bool commandable
False for a preset the device only reports (secured).
bool mode_observed
A device reading decides the card's mode; false when the mode is the last command (nothing reads it b...
bool current_temperature
The card shows a measured temperature.
HeatingProfileFeatures features
What the card and its companions offer.
bool fixed_range
TEMPERATURE_EXECUTE: the temperature range is fixed, so the YAML min_temperature: / max_temperature: ...
const HeatingModeEntry * modes
HVAC modes offered, with the value each writes.
uint8_t mp_prefix
High byte of a level or alias main parameter (0 for registers).
HeatingControlPath path
How the heater is commanded and read.
float visual_max_c
Highest target temperature the card offers (0 without a setpoint).
float visual_min_c
Lowest target temperature the card offers (0 without a setpoint).
size_t mode_count
Number of entries in modes.
const HeatingPresetEntry * presets
Presets offered.
size_t preset_count
Number of entries in presets.
HeatingFp setpoint_fp
TEMPERATURE_EXECUTE: the functional parameter sent with a setpoint.
What the hub has read from one heater so far.
int16_t level
Low byte of the main parameter of a heater commanded with a prefixed value: a heating level,...
float setpoint_c
Comfort setpoint as the heater holds it; NaN until read.
float room_temperature_c
Measured room temperature; NaN until read or on a sensor fault.
float max_setpoint_c
Highest setpoint the heater accepts; NaN until read.
int16_t mode
HeatingMode value; -1 until read.
int16_t regulator
HeatingRegulatorState value; -1 until read.
Runtime state of a paired IO‑Homecontrol device.
float temperature_range_min_c
Lower end of the temperature encoding range of a heater that carries a temperature in its main parame...
HeatingReadings heating
What the hub has read from a heater over the private register protocol (room temperature,...
uint8_t subtype
Device subtype (manufacturer‑specific).
float temperature_range_max_c
Upper end of that range (YAML max_temperature:).
DeviceType type
Device type (shutter, awning, etc.).