10#include "esphome/core/log.h"
20static const char *
const TAG =
"home_io_control.climate";
25struct ClimateModeMapping {
27 climate::ClimateMode climate_mode;
29constexpr ClimateModeMapping CLIMATE_MODE_MAP[] = {
36struct ClimatePresetMapping {
38 climate::ClimatePreset preset;
40constexpr ClimatePresetMapping CLIMATE_PRESET_MAP[] = {
48constexpr float CLIMATE_TARGET_TEMP_STEP_C = 0.5F;
51 for (
const auto &mapping : CLIMATE_MODE_MAP) {
52 if (mapping.hvac == hvac)
53 return mapping.climate_mode;
55 return climate::CLIMATE_MODE_OFF;
60 for (
const auto &mapping : CLIMATE_PRESET_MAP) {
62 return mapping.preset;
64 return climate::CLIMATE_PRESET_NONE;
87 "Device %s is declared as %s, which this climate entity cannot drive yet (it drives an electrical "
88 "heater: io_device_type: electrical_heater, io_subtype: 0 or 1) — disabling it",
106 if (profile->features.target_temperature)
117 climate::ClimateTraits
traits;
119 if (profile ==
nullptr) {
121 traits.add_supported_mode(climate::CLIMATE_MODE_OFF);
128 traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
130 traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
132 traits.add_supported_mode(to_climate_mode(entry.
hvac));
135 traits.add_supported_preset(to_climate_preset(entry.id));
142 traits.set_visual_target_temperature_step(CLIMATE_TARGET_TEMP_STEP_C);
149 if (profile ==
nullptr) {
150 ESP_LOGW(
TAG,
"Device %s: type not known yet, ignoring the request", this->
device_id_.c_str());
155 const bool mode_changed = this->
apply_mode_(*profile, call);
157 const bool preset_changed = this->
apply_preset_(*profile, call);
162 if (mode_changed || custom_preset_changed || preset_changed || temperature_changed)
163 this->publish_state();
167 switch (profile.
path) {
198 if (main_parameter == HEATING_MP_FROM_TARGET) {
202 float target = this->target_temperature;
203 if (std::isnan(target) && dev !=
nullptr)
205 if (std::isnan(target) || !this->
encode_target_(profile, target, main_parameter)) {
206 ESP_LOGW(
TAG,
"Device %s: no target temperature known yet, set one before selecting heat",
221 const auto &mode_opt = call.get_mode();
222 if (!mode_opt.has_value())
224 const climate::ClimateMode requested = *mode_opt;
226 if (to_climate_mode(entry.
hvac) != requested)
238 this->mode = requested;
243 this->clear_custom_preset_();
244 this->preset.reset();
257 if (!call.has_custom_preset())
259 const std::string custom = std::string(call.get_custom_preset());
269 const auto &preset_opt = call.get_preset();
270 if (!preset_opt.has_value())
272 const climate::ClimatePreset requested = *preset_opt;
283 ESP_LOGW(
TAG,
"Device %s: preset '%s' is reported by the device and cannot be set", this->
device_id_.c_str(),
301 this->preset = to_climate_preset(entry.
id);
302 this->clear_custom_preset_();
303 this->mode = climate::CLIMATE_MODE_HEAT;
312 const auto &target_opt = call.get_target_temperature();
313 if (!target_opt.has_value())
316 ESP_LOGW(
TAG,
"Device %s has no adjustable setpoint; the request is ignored", this->
device_id_.c_str());
319 const float target = *target_opt;
323 uint16_t main_parameter = 0;
329 this->target_temperature = target;
330 this->clear_custom_preset_();
331 this->preset.reset();
339 this->target_temperature = target;
346 bool changed =
false;
348 const auto differs = [](
float a,
float b) {
return !(std::isnan(a) && std::isnan(b)) && a != b; };
360 if (differs(this->target_temperature, target_c)) {
361 this->target_temperature = target_c;
385 climate::ClimateAction action = this->action;
386 if (this->mode == climate::CLIMATE_MODE_OFF && mode_known) {
387 action = climate::CLIMATE_ACTION_OFF;
391 ? climate::CLIMATE_ACTION_HEATING
392 : climate::CLIMATE_ACTION_IDLE;
395 action = readings.
working_rate > 0 ? climate::CLIMATE_ACTION_HEATING : climate::CLIMATE_ACTION_IDLE;
397 if (action == this->action)
399 this->action = action;
408 const bool period_shown = period.
active && !std::isnan(period.
target_c);
425 }
else if (preset_wire >= 0) {
431 if (this->has_custom_preset()) {
432 this->clear_custom_preset_();
435 if (this->preset.has_value()) {
436 this->preset.reset();
440 changed = this->
sync_action_(profile, readings, wire >= 0, seen) || changed;
444 if (!profile.
features.
mode_observed && !this->mode_commanded_ && seen && this->mode == climate::CLIMATE_MODE_OFF) {
445 this->mode = climate::CLIMATE_MODE_HEAT;
455 if (observed ==
nullptr)
459 bool changed =
false;
461 const climate::ClimateMode mode = to_climate_mode(observed->
hvac);
462 changed = mode != this->mode;
473 if (observed ==
nullptr)
479 bool changed =
false;
482 if (!this->has_custom_preset() || this->get_custom_preset() != shown->
custom_name) {
487 if (this->has_custom_preset()) {
488 this->clear_custom_preset_();
491 if (shown !=
nullptr) {
492 const climate::ClimatePreset preset = to_climate_preset(shown->
id);
493 if (!this->preset.has_value() || *this->preset != preset) {
494 this->preset = preset;
497 }
else if (this->preset.has_value()) {
499 this->preset.reset();
510 if (profile ==
nullptr)
513 this->publish_state();
517 LOG_CLIMATE(
"",
"IO-Homecontrol Climate",
this);
518 ESP_LOGCONFIG(
TAG,
" Device ID: %s", this->
device_id_.c_str());
519 ESP_LOGCONFIG(
TAG,
" Target temperature range: %.1f-%.1f C (maximum from the heater once read)",
HEATING_TEMP_MIN_C,
522 TAG,
" Status Poll Interval: %" PRIu32
" ms",
525 ESP_LOGCONFIG(
TAG,
" Model: %s", model->name);
530 const char *status =
"experimental, unvalidated on hardware";
533 ?
"readings and setpoint confirmed on hardware; mode and preset writes experimental"
534 :
"confirmed on hardware (setpoint, mode, presets, read-back)";
536 ESP_LOGCONFIG(
TAG,
" Status: %s", status);
uint32_t status_poll_interval_ms_
void register_device_binding_(Component *self, DeviceCapabilityClass entity_class, std::optional< bool > invert_override, std::function< void(const std::string &, const IoDevice &)> on_update, bool dimmable=false)
Perform the shared setup() registration ritual.
IOHomeControlComponent * parent_
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).
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; "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...
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.
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...
@ WORKING_RATE
The working rate (HeatingFunction::WORKING_RATE): heating above 0 %.
@ REGULATOR
The regulator register (HeatingFunction::REGULATOR_STATE): heating at state 2.
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.
@ HEATING
The element is heating.
const HeatingProfile * find_heating_profile(DeviceType type, uint8_t subtype, DeviceModel model)
Look up the heating profile for a (type, subtype, model).
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.
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).
@ NONE
No preset: selecting it ends the one that runs (a towel dryer's boost or drying).
const DeviceTypeInfo * find_device_model_info(DeviceType type, uint8_t subtype, DeviceModel model)
Look up the model row with exactly this (type, subtype, model), without falling back.
@ 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.
const HeatingObservedState * find_observed_preset(const HeatingProfile &profile, uint8_t wire)
Find what a device-reported value of the preset register (preset_fn) shows on the card.
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.
ProgrammePeriodState resolve_programme_period(const HeatingProfile &profile, const HeatingReadings &readings)
Which period of its programme a heater is in, and the temperature it heats to.
@ CLIMATE
Climate device (heating/cooling).
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.
@ AUTO
The device decides.
static const char *const TAG
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 row of the node-type / subtype table.
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.
HeatingHvacMode hvac
HVAC mode shown.
One preset the card offers.
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.
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 target_temperature
The card has a setpoint.
bool action
The card shows heating / idle.
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: ...
uint8_t mp_prefix
High byte of a level or alias main parameter (0 for registers).
std::span< const HeatingPresetEntry > presets
Presets offered.
HeatingActionSource action_source
Where features.action reads from.
HeatingControlPath path
How the heater is commanded and read.
float visual_max_c
Highest target temperature the card offers (0 without a setpoint).
bool experimental_writes
PRIVATE_REGISTERS: the mode and preset writes are unconfirmed on a device.
float visual_min_c
Lowest target temperature the card offers (0 without a setpoint).
HeatingFunction preset_fn
PRIVATE_REGISTERS with preset_observed: the register the card's preset is read from.
HeatingFunction mode_fn
PRIVATE_REGISTERS: the register the HVAC modes write and the card's mode is read from.
std::span< const HeatingObservedState > preset_observed
Device-reported value of preset_fn -> preset; empty when the mode register names the preset too (obse...
std::span< const HeatingModeEntry > modes
HVAC modes offered, with the value each writes.
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,...
int16_t value_of(HeatingFunction fn) const
The stored integer value of a register (a mode, a state, a flag, a percentage).
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.
int16_t working_rate
Working rate, 0-100 %; -1 until read.
float max_setpoint_c
Highest setpoint the heater accepts; NaN 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,...
float temperature_range_max_c
Upper end of that range (YAML max_temperature:).
What a heater's programme period resolves to, from its readings.
int8_t preset_index
Index into the profile's presets of the period, or -1 when the temperature in force fits neither peri...
bool active
The programme runs (and no timed run is on top of it).
float target_c
The temperature in force; NaN until it has been read.