From 72d6471ab8cab724c0fc805ed7a5bb216811652b Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 18 Oct 2019 10:39:14 +0200 Subject: [PATCH] add support for climate action (#720) * add support for climate action: Following hass implementation of climate, action represents the current action the climate device is perfoming, e.g. cooling or heating fix bang_bang climate: make sure that the thresholds are always respected. fixes the issue where the component would just keep on heating, regardless of the temperature range * Updates - Use dedicated enum for action (otherwise it gets confusing because "auto" is not a valid action) - Add field to tell HA that action is supported - Revert semantic changes in bang_bang * Conditional print Co-authored-by: Otto Winter --- esphome/components/api/api.proto | 8 ++++ esphome/components/api/api_connection.cpp | 2 + esphome/components/api/api_pb2.cpp | 30 +++++++++++++++ esphome/components/api/api_pb2.h | 7 ++++ .../bang_bang/bang_bang_climate.cpp | 37 +++++++++---------- .../components/bang_bang/bang_bang_climate.h | 7 +--- esphome/components/climate/climate.cpp | 3 ++ esphome/components/climate/climate.h | 2 + esphome/components/climate/climate_mode.cpp | 12 ++++++ esphome/components/climate/climate_mode.h | 11 ++++++ esphome/components/climate/climate_traits.cpp | 2 + esphome/components/climate/climate_traits.h | 5 +++ 12 files changed, 101 insertions(+), 25 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index c776a96f86..e454bf1d31 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -644,6 +644,12 @@ enum ClimateMode { CLIMATE_MODE_COOL = 2; CLIMATE_MODE_HEAT = 3; } +enum ClimateAction { + CLIMATE_ACTION_OFF = 0; + // values same as mode for readability + CLIMATE_ACTION_COOLING = 2; + CLIMATE_ACTION_HEATING = 3; +} message ListEntitiesClimateResponse { option (id) = 46; option (source) = SOURCE_SERVER; @@ -661,6 +667,7 @@ message ListEntitiesClimateResponse { float visual_max_temperature = 9; float visual_temperature_step = 10; bool supports_away = 11; + bool supports_action = 12; } message ClimateStateResponse { option (id) = 47; @@ -675,6 +682,7 @@ message ClimateStateResponse { float target_temperature_low = 5; float target_temperature_high = 6; bool away = 7; + ClimateAction action = 8; } message ClimateCommandRequest { option (id) = 48; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 76cf72f8ff..4a595a3f99 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -442,6 +442,7 @@ bool APIConnection::send_climate_state(climate::Climate *climate) { ClimateStateResponse resp{}; resp.key = climate->get_object_id_hash(); resp.mode = static_cast(climate->mode); + resp.action = static_cast(climate->action); if (traits.get_supports_current_temperature()) resp.current_temperature = climate->current_temperature; if (traits.get_supports_two_point_target_temperature()) { @@ -472,6 +473,7 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_temperature_step = traits.get_visual_temperature_step(); msg.supports_away = traits.get_supports_away(); + msg.supports_action = traits.get_supports_action(); return this->send_list_entities_climate_response(msg); } void APIConnection::climate_command(const ClimateCommandRequest &msg) { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 815feedea8..c4fa89ef97 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -106,6 +106,18 @@ template<> const char *proto_enum_to_string(EnumClimateMode val return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(EnumClimateAction value) { + switch (value) { + case CLIMATE_ACTION_OFF: + return "CLIMATE_ACTION_OFF"; + case CLIMATE_ACTION_COOLING: + return "CLIMATE_ACTION_COOLING"; + case CLIMATE_ACTION_HEATING: + return "CLIMATE_ACTION_HEATING"; + default: + return "UNKNOWN"; + } +} bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -2394,6 +2406,10 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v this->supports_away = value.as_bool(); return true; } + case 12: { + this->supports_action = value.as_bool(); + return true; + } default: return false; } @@ -2452,6 +2468,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(9, this->visual_max_temperature); buffer.encode_float(10, this->visual_temperature_step); buffer.encode_bool(11, this->supports_away); + buffer.encode_bool(12, this->supports_action); } void ListEntitiesClimateResponse::dump_to(std::string &out) const { char buffer[64]; @@ -2505,6 +2522,10 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append(" supports_away: "); out.append(YESNO(this->supports_away)); out.append("\n"); + + out.append(" supports_action: "); + out.append(YESNO(this->supports_action)); + out.append("\n"); out.append("}"); } bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -2517,6 +2538,10 @@ bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->away = value.as_bool(); return true; } + case 8: { + this->action = value.as_enum(); + return true; + } default: return false; } @@ -2555,6 +2580,7 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(5, this->target_temperature_low); buffer.encode_float(6, this->target_temperature_high); buffer.encode_bool(7, this->away); + buffer.encode_enum(8, this->action); } void ClimateStateResponse::dump_to(std::string &out) const { char buffer[64]; @@ -2591,6 +2617,10 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append(" away: "); out.append(YESNO(this->away)); out.append("\n"); + + out.append(" action: "); + out.append(proto_enum_to_string(this->action)); + out.append("\n"); out.append("}"); } bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 50bf3117c0..9997a68477 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -49,6 +49,11 @@ enum EnumClimateMode : uint32_t { CLIMATE_MODE_COOL = 2, CLIMATE_MODE_HEAT = 3, }; +enum EnumClimateAction : uint32_t { + CLIMATE_ACTION_OFF = 0, + CLIMATE_ACTION_COOLING = 2, + CLIMATE_ACTION_HEATING = 3, +}; class HelloRequest : public ProtoMessage { public: std::string client_info{}; // NOLINT @@ -638,6 +643,7 @@ class ListEntitiesClimateResponse : public ProtoMessage { float visual_max_temperature{0.0f}; // NOLINT float visual_temperature_step{0.0f}; // NOLINT bool supports_away{false}; // NOLINT + bool supports_action{false}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -655,6 +661,7 @@ class ClimateStateResponse : public ProtoMessage { float target_temperature_low{0.0f}; // NOLINT float target_temperature_high{0.0f}; // NOLINT bool away{false}; // NOLINT + EnumClimateAction action{}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index 1bdabaec37..17c5c0bc48 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -46,52 +46,51 @@ climate::ClimateTraits BangBangClimate::traits() { traits.set_supports_heat_mode(this->supports_heat_); traits.set_supports_two_point_target_temperature(true); traits.set_supports_away(this->supports_away_); + traits.set_supports_action(true); return traits; } void BangBangClimate::compute_state_() { if (this->mode != climate::CLIMATE_MODE_AUTO) { // in non-auto mode - this->switch_to_mode_(this->mode); + this->switch_to_action_(static_cast(this->mode)); return; } - - // auto mode, compute target mode if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || isnan(this->target_temperature_high)) { // if any control values are nan, go to OFF (idle) mode - this->switch_to_mode_(climate::CLIMATE_MODE_OFF); + this->switch_to_action_(climate::CLIMATE_ACTION_OFF); return; } const bool too_cold = this->current_temperature < this->target_temperature_low; const bool too_hot = this->current_temperature > this->target_temperature_high; - climate::ClimateMode target_mode; + climate::ClimateAction target_action; if (too_cold) { // too cold -> enable heating if possible, else idle if (this->supports_heat_) - target_mode = climate::CLIMATE_MODE_HEAT; + target_action = climate::CLIMATE_ACTION_HEATING; else - target_mode = climate::CLIMATE_MODE_OFF; + target_action = climate::CLIMATE_ACTION_OFF; } else if (too_hot) { // too hot -> enable cooling if possible, else idle if (this->supports_cool_) - target_mode = climate::CLIMATE_MODE_COOL; + target_action = climate::CLIMATE_ACTION_COOLING; else - target_mode = climate::CLIMATE_MODE_OFF; + target_action = climate::CLIMATE_ACTION_OFF; } else { // neither too hot nor too cold -> in range if (this->supports_cool_ && this->supports_heat_) { // if supports both ends, go to idle mode - target_mode = climate::CLIMATE_MODE_OFF; + target_action = climate::CLIMATE_ACTION_OFF; } else { // else use current mode and don't change (hysteresis) - target_mode = this->internal_mode_; + target_action = this->action; } } - this->switch_to_mode_(target_mode); + this->switch_to_action_(target_action); } -void BangBangClimate::switch_to_mode_(climate::ClimateMode mode) { - if (mode == this->internal_mode_) +void BangBangClimate::switch_to_action_(climate::ClimateAction action) { + if (action == this->action) // already in target mode return; @@ -100,14 +99,14 @@ void BangBangClimate::switch_to_mode_(climate::ClimateMode mode) { this->prev_trigger_ = nullptr; } Trigger<> *trig; - switch (mode) { - case climate::CLIMATE_MODE_OFF: + switch (action) { + case climate::CLIMATE_ACTION_OFF: trig = this->idle_trigger_; break; - case climate::CLIMATE_MODE_COOL: + case climate::CLIMATE_ACTION_COOLING: trig = this->cool_trigger_; break; - case climate::CLIMATE_MODE_HEAT: + case climate::CLIMATE_ACTION_HEATING: trig = this->heat_trigger_; break; default: @@ -116,7 +115,7 @@ void BangBangClimate::switch_to_mode_(climate::ClimateMode mode) { if (trig != nullptr) { // trig should never be null, but still check so that we don't crash trig->trigger(); - this->internal_mode_ = mode; + this->action = action; this->prev_trigger_ = trig; this->publish_state(); } diff --git a/esphome/components/bang_bang/bang_bang_climate.h b/esphome/components/bang_bang/bang_bang_climate.h index 716655d20f..0a79c1d7af 100644 --- a/esphome/components/bang_bang/bang_bang_climate.h +++ b/esphome/components/bang_bang/bang_bang_climate.h @@ -43,7 +43,7 @@ class BangBangClimate : public climate::Climate, public Component { void compute_state_(); /// Switch the climate device to the given climate mode. - void switch_to_mode_(climate::ClimateMode mode); + void switch_to_action_(climate::ClimateAction action); /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; @@ -74,11 +74,6 @@ class BangBangClimate : public climate::Climate, public Component { * This is so that the previous trigger can be stopped before enabling a new one. */ Trigger<> *prev_trigger_{nullptr}; - /** The climate mode that is currently active - for a `.mode = AUTO` this will - * contain the actual mode the device - * - */ - climate::ClimateMode internal_mode_{climate::CLIMATE_MODE_OFF}; BangBangClimateTargetTempConfig normal_config_{}; bool supports_away_{false}; diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 2b40f932f9..7c7da6bb0c 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -173,6 +173,9 @@ void Climate::publish_state() { auto traits = this->get_traits(); ESP_LOGD(TAG, " Mode: %s", climate_mode_to_string(this->mode)); + if (traits.get_supports_action()) { + ESP_LOGD(TAG, " Action: %s", climate_action_to_string(this->action)); + } if (traits.get_supports_current_temperature()) { ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature); } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index c58eed1a7c..70c6bef13b 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -121,6 +121,8 @@ class Climate : public Nameable { /// The active mode of the climate device. ClimateMode mode{CLIMATE_MODE_OFF}; + /// The active state of the climate device. + ClimateAction action{CLIMATE_ACTION_OFF}; /// The current temperature of the climate device, as reported from the integration. float current_temperature{NAN}; diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index 07b97f4f33..34aa564fb0 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -17,6 +17,18 @@ const char *climate_mode_to_string(ClimateMode mode) { return "UNKNOWN"; } } +const char *climate_action_to_string(ClimateAction action) { + switch (action) { + case CLIMATE_ACTION_OFF: + return "OFF"; + case CLIMATE_ACTION_COOLING: + return "COOLING"; + case CLIMATE_ACTION_HEATING: + return "HEATING"; + default: + return "UNKNOWN"; + } +} } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index 28608b7cd8..e5786286d8 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -17,8 +17,19 @@ enum ClimateMode : uint8_t { CLIMATE_MODE_HEAT = 3, }; +/// Enum for the current action of the climate device. Values match those of ClimateMode. +enum ClimateAction : uint8_t { + /// The climate device is off (inactive or no power) + CLIMATE_ACTION_OFF = 0, + /// The climate device is actively cooling (usually in cool or auto mode) + CLIMATE_ACTION_COOLING = 2, + /// The climate device is actively heating (usually in heat or auto mode) + CLIMATE_ACTION_HEATING = 3, +}; + /// Convert the given ClimateMode to a human-readable string. const char *climate_mode_to_string(ClimateMode mode); +const char *climate_action_to_string(ClimateAction action); } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp index 712186aa80..a1db2bc696 100644 --- a/esphome/components/climate/climate_traits.cpp +++ b/esphome/components/climate/climate_traits.cpp @@ -30,6 +30,7 @@ void ClimateTraits::set_supports_auto_mode(bool supports_auto_mode) { supports_a void ClimateTraits::set_supports_cool_mode(bool supports_cool_mode) { supports_cool_mode_ = supports_cool_mode; } void ClimateTraits::set_supports_heat_mode(bool supports_heat_mode) { supports_heat_mode_ = supports_heat_mode; } void ClimateTraits::set_supports_away(bool supports_away) { supports_away_ = supports_away; } +void ClimateTraits::set_supports_action(bool supports_action) { supports_action_ = supports_action; } float ClimateTraits::get_visual_min_temperature() const { return visual_min_temperature_; } void ClimateTraits::set_visual_min_temperature(float visual_min_temperature) { visual_min_temperature_ = visual_min_temperature; @@ -52,6 +53,7 @@ int8_t ClimateTraits::get_temperature_accuracy_decimals() const { } void ClimateTraits::set_visual_temperature_step(float temperature_step) { visual_temperature_step_ = temperature_step; } bool ClimateTraits::get_supports_away() const { return supports_away_; } +bool ClimateTraits::get_supports_action() const { return supports_action_; } } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 34e03455b1..2d6f44eea6 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -23,6 +23,8 @@ namespace climate { * - heat mode (increases current temperature) * - supports away - away mode means that the climate device supports two different * target temperature settings: one target temp setting for "away" mode and one for non-away mode. + * - supports action - if the climate device supports reporting the active + * current action of the device with the action property. * * This class also contains static data for the climate device display: * - visual min/max temperature - tells the frontend what range of temperatures the climate device @@ -41,6 +43,8 @@ class ClimateTraits { void set_supports_heat_mode(bool supports_heat_mode); void set_supports_away(bool supports_away); bool get_supports_away() const; + void set_supports_action(bool supports_action); + bool get_supports_action() const; bool supports_mode(ClimateMode mode) const; float get_visual_min_temperature() const; @@ -58,6 +62,7 @@ class ClimateTraits { bool supports_cool_mode_{false}; bool supports_heat_mode_{false}; bool supports_away_{false}; + bool supports_action_{false}; float visual_min_temperature_{10}; float visual_max_temperature_{30};