diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index ffb3bcb07e..eb639f2065 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -829,7 +829,7 @@ message ListEntitiesClimateResponse { repeated ClimateMode supported_modes = 7; float visual_min_temperature = 8; float visual_max_temperature = 9; - float visual_temperature_step = 10; + float visual_target_temperature_step = 10; // for older peer versions - in new system this // is if CLIMATE_PRESET_AWAY exists is supported_presets bool legacy_supports_away = 11; @@ -842,6 +842,7 @@ message ListEntitiesClimateResponse { bool disabled_by_default = 18; string icon = 19; EntityCategory entity_category = 20; + float visual_current_temperature_step = 21; } message ClimateStateResponse { option (id) = 47; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 65659941d6..487aa53193 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -548,7 +548,9 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { msg.visual_min_temperature = traits.get_visual_min_temperature(); msg.visual_max_temperature = traits.get_visual_max_temperature(); - msg.visual_temperature_step = traits.get_visual_temperature_step(); + msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); + msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); + msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY); msg.supports_action = traits.get_supports_action(); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 9df05d2978..3fc1bfa95d 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3451,7 +3451,11 @@ bool ListEntitiesClimateResponse::decode_32bit(uint32_t field_id, Proto32Bit val return true; } case 10: { - this->visual_temperature_step = value.as_float(); + this->visual_target_temperature_step = value.as_float(); + return true; + } + case 21: { + this->visual_current_temperature_step = value.as_float(); return true; } default: @@ -3470,7 +3474,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { } buffer.encode_float(8, this->visual_min_temperature); buffer.encode_float(9, this->visual_max_temperature); - buffer.encode_float(10, this->visual_temperature_step); + buffer.encode_float(10, this->visual_target_temperature_step); buffer.encode_bool(11, this->legacy_supports_away); buffer.encode_bool(12, this->supports_action); for (auto &it : this->supported_fan_modes) { @@ -3491,6 +3495,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(18, this->disabled_by_default); buffer.encode_string(19, this->icon); buffer.encode_enum(20, this->entity_category); + buffer.encode_float(21, this->visual_current_temperature_step); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesClimateResponse::dump_to(std::string &out) const { @@ -3537,8 +3542,8 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" visual_temperature_step: "); - sprintf(buffer, "%g", this->visual_temperature_step); + out.append(" visual_target_temperature_step: "); + sprintf(buffer, "%g", this->visual_target_temperature_step); out.append(buffer); out.append("\n"); @@ -3591,6 +3596,11 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" visual_current_temperature_step: "); + sprintf(buffer, "%g", this->visual_current_temperature_step); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 2db1c6fafa..e192892e72 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -915,7 +915,7 @@ class ListEntitiesClimateResponse : public ProtoMessage { std::vector supported_modes{}; float visual_min_temperature{0.0f}; float visual_max_temperature{0.0f}; - float visual_temperature_step{0.0f}; + float visual_target_temperature_step{0.0f}; bool legacy_supports_away{false}; bool supports_action{false}; std::vector supported_fan_modes{}; @@ -926,6 +926,7 @@ class ListEntitiesClimateResponse : public ProtoMessage { bool disabled_by_default{false}; std::string icon{}; enums::EntityCategory entity_category{}; + float visual_current_temperature_step{0.0f}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index eaa87afcb1..709d0d12ed 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -104,10 +104,40 @@ CLIMATE_SWING_MODES = { validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) +CONF_CURRENT_TEMPERATURE = "current_temperature" + +visual_temperature = cv.float_with_unit( + "visual_temperature", "(°C|° C|°|C|° K|° K|K|°F|° F|F)?" +) + + +def single_visual_temperature(value): + if isinstance(value, dict): + return value + + value = visual_temperature(value) + return VISUAL_TEMPERATURE_STEP_SCHEMA( + { + CONF_TARGET_TEMPERATURE: value, + CONF_CURRENT_TEMPERATURE: value, + } + ) + + # Actions ControlAction = climate_ns.class_("ControlAction", automation.Action) StateTrigger = climate_ns.class_("StateTrigger", automation.Trigger.template()) +VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Any( + single_visual_temperature, + cv.Schema( + { + cv.Required(CONF_TARGET_TEMPERATURE): visual_temperature, + cv.Required(CONF_CURRENT_TEMPERATURE): visual_temperature, + } + ), +) + CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { cv.GenerateID(): cv.declare_id(Climate), @@ -116,9 +146,7 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA). { cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature, cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature, - cv.Optional(CONF_TEMPERATURE_STEP): cv.float_with_unit( - "visual_temperature", "(°C|° C|°|C|° K|° K|K|°F|° F|F)?" - ), + cv.Optional(CONF_TEMPERATURE_STEP): VISUAL_TEMPERATURE_STEP_SCHEMA, } ), cv.Optional(CONF_ACTION_STATE_TOPIC): cv.All( @@ -193,7 +221,12 @@ async def setup_climate_core_(var, config): if CONF_MAX_TEMPERATURE in visual: cg.add(var.set_visual_max_temperature_override(visual[CONF_MAX_TEMPERATURE])) if CONF_TEMPERATURE_STEP in visual: - cg.add(var.set_visual_temperature_step_override(visual[CONF_TEMPERATURE_STEP])) + cg.add( + var.set_visual_temperature_step_override( + visual[CONF_TEMPERATURE_STEP][CONF_TARGET_TEMPERATURE], + visual[CONF_TEMPERATURE_STEP][CONF_CURRENT_TEMPERATURE], + ) + ) if CONF_MQTT_ID in config: mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index e1611d2fa9..b80fe640c8 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -430,9 +430,11 @@ ClimateTraits Climate::get_traits() { if (this->visual_max_temperature_override_.has_value()) { traits.set_visual_max_temperature(*this->visual_max_temperature_override_); } - if (this->visual_temperature_step_override_.has_value()) { - traits.set_visual_temperature_step(*this->visual_temperature_step_override_); + if (this->visual_target_temperature_step_override_.has_value()) { + traits.set_visual_target_temperature_step(*this->visual_target_temperature_step_override_); + traits.set_visual_current_temperature_step(*this->visual_current_temperature_step_override_); } + return traits; } @@ -442,8 +444,9 @@ void Climate::set_visual_min_temperature_override(float visual_min_temperature_o void Climate::set_visual_max_temperature_override(float visual_max_temperature_override) { this->visual_max_temperature_override_ = visual_max_temperature_override; } -void Climate::set_visual_temperature_step_override(float visual_temperature_step_override) { - this->visual_temperature_step_override_ = visual_temperature_step_override; +void Climate::set_visual_temperature_step_override(float target, float current) { + this->visual_target_temperature_step_override_ = target; + this->visual_current_temperature_step_override_ = current; } #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" @@ -541,7 +544,9 @@ void Climate::dump_traits_(const char *tag) { ESP_LOGCONFIG(tag, " [x] Visual settings:"); ESP_LOGCONFIG(tag, " - Min: %.1f", traits.get_visual_min_temperature()); ESP_LOGCONFIG(tag, " - Max: %.1f", traits.get_visual_max_temperature()); - ESP_LOGCONFIG(tag, " - Step: %.1f", traits.get_visual_temperature_step()); + ESP_LOGCONFIG(tag, " - Step:"); + ESP_LOGCONFIG(tag, " Target: %.1f", traits.get_visual_target_temperature_step()); + ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step()); if (traits.get_supports_current_temperature()) { ESP_LOGCONFIG(tag, " [x] Supports current temperature"); } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index d508bb31b0..8cc260abbe 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -241,7 +241,7 @@ class Climate : public EntityBase { void set_visual_min_temperature_override(float visual_min_temperature_override); void set_visual_max_temperature_override(float visual_max_temperature_override); - void set_visual_temperature_step_override(float visual_temperature_step_override); + void set_visual_temperature_step_override(float target, float current); protected: friend ClimateCall; @@ -288,7 +288,8 @@ class Climate : public EntityBase { ESPPreferenceObject rtc_; optional visual_min_temperature_override_{}; optional visual_max_temperature_override_{}; - optional visual_temperature_step_override_{}; + optional visual_target_temperature_step_override_{}; + optional visual_current_temperature_step_override_{}; }; } // namespace climate diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp index 38ded6cdf7..342dffaad6 100644 --- a/esphome/components/climate/climate_traits.cpp +++ b/esphome/components/climate/climate_traits.cpp @@ -3,8 +3,12 @@ namespace esphome { namespace climate { -int8_t ClimateTraits::get_temperature_accuracy_decimals() const { - return step_to_accuracy_decimals(this->visual_temperature_step_); +int8_t ClimateTraits::get_target_temperature_accuracy_decimals() const { + return step_to_accuracy_decimals(this->visual_target_temperature_step_); +} + +int8_t ClimateTraits::get_current_temperature_accuracy_decimals() const { + return step_to_accuracy_decimals(this->visual_current_temperature_step_); } } // namespace climate diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 9da9bb7374..ffbd8c5ae0 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -147,9 +147,20 @@ class ClimateTraits { void set_visual_min_temperature(float visual_min_temperature) { visual_min_temperature_ = visual_min_temperature; } float get_visual_max_temperature() const { return visual_max_temperature_; } void set_visual_max_temperature(float visual_max_temperature) { visual_max_temperature_ = visual_max_temperature; } - float get_visual_temperature_step() const { return visual_temperature_step_; } - int8_t get_temperature_accuracy_decimals() const; - void set_visual_temperature_step(float temperature_step) { visual_temperature_step_ = temperature_step; } + float get_visual_target_temperature_step() const { return visual_target_temperature_step_; } + float get_visual_current_temperature_step() const { return visual_current_temperature_step_; } + void set_visual_target_temperature_step(float temperature_step) { + visual_target_temperature_step_ = temperature_step; + } + void set_visual_current_temperature_step(float temperature_step) { + visual_current_temperature_step_ = temperature_step; + } + void set_visual_temperature_step(float temperature_step) { + visual_target_temperature_step_ = temperature_step; + visual_current_temperature_step_ = temperature_step; + } + int8_t get_target_temperature_accuracy_decimals() const; + int8_t get_current_temperature_accuracy_decimals() const; protected: void set_mode_support_(climate::ClimateMode mode, bool supported) { @@ -186,7 +197,8 @@ class ClimateTraits { float visual_min_temperature_{10}; float visual_max_temperature_{30}; - float visual_temperature_step_{0.1}; + float visual_target_temperature_step_{0.1}; + float visual_current_temperature_step_{0.1}; }; } // namespace climate diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 1947e02b9e..e88ffcc37c 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -62,7 +62,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo // max_temp root[MQTT_MAX_TEMP] = traits.get_visual_max_temperature(); // temp_step - root["temp_step"] = traits.get_visual_temperature_step(); + root["temp_step"] = traits.get_visual_target_temperature_step(); // temperature units are always coerced to Celsius internally root[MQTT_TEMPERATURE_UNIT] = "C"; @@ -281,21 +281,22 @@ bool MQTTClimateComponent::publish_state_() { bool success = true; if (!this->publish(this->get_mode_state_topic(), mode_s)) success = false; - int8_t accuracy = traits.get_temperature_accuracy_decimals(); + int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); + int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); if (traits.get_supports_current_temperature() && !std::isnan(this->device_->current_temperature)) { - std::string payload = value_accuracy_to_string(this->device_->current_temperature, accuracy); + std::string payload = value_accuracy_to_string(this->device_->current_temperature, current_accuracy); if (!this->publish(this->get_current_temperature_state_topic(), payload)) success = false; } if (traits.get_supports_two_point_target_temperature()) { - std::string payload = value_accuracy_to_string(this->device_->target_temperature_low, accuracy); + std::string payload = value_accuracy_to_string(this->device_->target_temperature_low, target_accuracy); if (!this->publish(this->get_target_temperature_low_state_topic(), payload)) success = false; - payload = value_accuracy_to_string(this->device_->target_temperature_high, accuracy); + payload = value_accuracy_to_string(this->device_->target_temperature_high, target_accuracy); if (!this->publish(this->get_target_temperature_high_state_topic(), payload)) success = false; } else { - std::string payload = value_accuracy_to_string(this->device_->target_temperature, accuracy); + std::string payload = value_accuracy_to_string(this->device_->target_temperature, target_accuracy); if (!this->publish(this->get_target_temperature_state_topic(), payload)) success = false; } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 513399e257..6c74c79ce6 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -873,7 +873,8 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf return json::build_json([obj, start_config](JsonObject root) { set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); const auto traits = obj->get_traits(); - int8_t accuracy = traits.get_temperature_accuracy_decimals(); + int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); + int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); char __buf[16]; if (start_config == DETAIL_ALL) { @@ -910,9 +911,9 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf bool has_state = false; root["mode"] = PSTR_LOCAL(climate_mode_to_string(obj->mode)); - root["max_temp"] = value_accuracy_to_string(traits.get_visual_max_temperature(), accuracy); - root["min_temp"] = value_accuracy_to_string(traits.get_visual_min_temperature(), accuracy); - root["step"] = traits.get_visual_temperature_step(); + root["max_temp"] = value_accuracy_to_string(traits.get_visual_max_temperature(), target_accuracy); + root["min_temp"] = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy); + root["step"] = traits.get_visual_target_temperature_step(); if (traits.get_supports_action()) { root["action"] = PSTR_LOCAL(climate_action_to_string(obj->action)); root["state"] = root["action"]; @@ -935,20 +936,20 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf } if (traits.get_supports_current_temperature()) { if (!std::isnan(obj->current_temperature)) { - root["current_temperature"] = value_accuracy_to_string(obj->current_temperature, accuracy); + root["current_temperature"] = value_accuracy_to_string(obj->current_temperature, current_accuracy); } else { root["current_temperature"] = "NA"; } } if (traits.get_supports_two_point_target_temperature()) { - root["target_temperature_low"] = value_accuracy_to_string(obj->target_temperature_low, accuracy); - root["target_temperature_high"] = value_accuracy_to_string(obj->target_temperature_high, accuracy); + root["target_temperature_low"] = value_accuracy_to_string(obj->target_temperature_low, target_accuracy); + root["target_temperature_high"] = value_accuracy_to_string(obj->target_temperature_high, target_accuracy); if (!has_state) { - root["state"] = - value_accuracy_to_string((obj->target_temperature_high + obj->target_temperature_low) / 2.0f, accuracy); + root["state"] = value_accuracy_to_string((obj->target_temperature_high + obj->target_temperature_low) / 2.0f, + target_accuracy); } } else { - root["target_temperature"] = value_accuracy_to_string(obj->target_temperature, accuracy); + root["target_temperature"] = value_accuracy_to_string(obj->target_temperature, target_accuracy); if (!has_state) root["state"] = root["target_temperature"]; }