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;
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",
115 if (profile->features.target_temperature)
117 for (
size_t i = 0; i < profile->preset_count; i++) {
126 climate::ClimateTraits
traits;
128 if (profile ==
nullptr) {
130 traits.add_supported_mode(climate::CLIMATE_MODE_OFF);
137 traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
139 traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
140 for (
size_t i = 0; i < profile->
mode_count; i++)
144 traits.add_supported_preset(to_climate_preset(profile->
presets[i].
id));
151 traits.set_visual_target_temperature_step(CLIMATE_TARGET_TEMP_STEP_C);
158 if (profile ==
nullptr) {
159 ESP_LOGW(
TAG,
"Device %s: type not known yet, ignoring the request", this->
device_id_.c_str());
164 const bool mode_changed = this->
apply_mode_(*profile, call);
166 const bool preset_changed = this->
apply_preset_(*profile, call);
171 if (mode_changed || custom_preset_changed || preset_changed || temperature_changed)
172 this->publish_state();
176 switch (profile.
path) {
207 if (main_parameter == HEATING_MP_FROM_TARGET) {
211 float target = this->target_temperature;
212 if (std::isnan(target) && dev !=
nullptr)
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",
230 const auto &mode_opt = call.get_mode();
231 if (!mode_opt.has_value())
233 const climate::ClimateMode requested = *mode_opt;
234 for (
size_t i = 0; i < profile.
mode_count; i++) {
236 if (to_climate_mode(entry.
hvac) != requested)
248 this->mode = requested;
252 this->clear_custom_preset_();
253 if (has_observed_standard_preset(profile))
254 this->preset.reset();
267 if (!call.has_custom_preset())
269 const std::string custom = std::string(call.get_custom_preset());
275 ESP_LOGW(
TAG,
"Device %s: preset '%s' is reported by the device and cannot be set", this->
device_id_.c_str(),
294 const auto &preset_opt = call.get_preset();
295 if (!preset_opt.has_value())
297 const climate::ClimatePreset requested = *preset_opt;
302 if (!entry.
commandable || !this->write_preset_(profile, entry))
307 this->preset = requested;
308 this->clear_custom_preset_();
310 this->mode = climate::CLIMATE_MODE_HEAT;
322 const auto &target_opt = call.get_target_temperature();
323 if (!target_opt.has_value())
326 ESP_LOGW(
TAG,
"Device %s has no adjustable setpoint; the request is ignored", this->
device_id_.c_str());
329 const float target = *target_opt;
333 uint16_t main_parameter = 0;
339 this->target_temperature = target;
340 this->clear_custom_preset_();
341 this->preset.reset();
349 this->target_temperature = target;
355 bool changed =
false;
357 const auto differs = [](
float a,
float b) {
return !(std::isnan(a) && std::isnan(b)) && a != b; };
369 if (differs(this->target_temperature, readings.
setpoint_c)) {
370 this->target_temperature = readings.
setpoint_c;
394 climate::ClimateAction action = this->action;
395 if (this->mode == climate::CLIMATE_MODE_OFF && mode_known) {
396 action = climate::CLIMATE_ACTION_OFF;
400 ? climate::CLIMATE_ACTION_HEATING
401 : climate::CLIMATE_ACTION_IDLE;
403 if (action == this->action)
405 this->action = action;
420 if (this->has_custom_preset()) {
421 this->clear_custom_preset_();
424 if (this->preset.has_value()) {
425 this->preset.reset();
429 changed = this->
sync_action_(profile, readings, wire >= 0, seen) || changed;
433 if (!profile.
features.
mode_observed && !this->mode_commanded_ && seen && this->mode == climate::CLIMATE_MODE_OFF) {
434 this->mode = climate::CLIMATE_MODE_HEAT;
444 if (observed ==
nullptr)
448 bool changed =
false;
450 const climate::ClimateMode mode = to_climate_mode(observed->
hvac);
451 changed = mode != this->mode;
457 if (!this->has_custom_preset() || this->get_custom_preset() != shown->
custom_name) {
462 if (this->has_custom_preset()) {
463 this->clear_custom_preset_();
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;
472 }
else if (has_observed_standard_preset(profile) && this->preset.has_value()) {
474 this->preset.reset();
485 if (profile ==
nullptr)
488 this->publish_state();
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,
497 TAG,
" Status Poll Interval: %" PRIu32
" ms",
502 ESP_LOGCONFIG(
TAG,
" Status: %s",
503 confirmed ?
"confirmed on hardware (setpoint, mode, read-back); Home/Away presets untested"
504 :
"experimental, unvalidated on hardware");
void register_device_binding_(Component *self, bool inverted, std::function< void(const std::string &, const IoDevice &)> on_update)
Perform the shared setup() registration ritual.
uint32_t status_poll_interval_ms_
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.
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.
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.
@ HEATING
The element is heating.
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.
@ HOME
Someone is in the room.
@ 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.
@ 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 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.
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 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: ...
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.).