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 <otto@otto-winter.com>
This commit is contained in:
Marcel van der Veldt 2019-10-18 10:39:14 +02:00 committed by Otto Winter
parent 22aecdfc6f
commit 72d6471ab8
12 changed files with 101 additions and 25 deletions

View file

@ -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;

View file

@ -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<EnumClimateMode>(climate->mode);
resp.action = static_cast<EnumClimateAction>(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) {

View file

@ -106,6 +106,18 @@ template<> const char *proto_enum_to_string<EnumClimateMode>(EnumClimateMode val
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<EnumClimateAction>(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<EnumClimateAction>();
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<EnumClimateAction>(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<EnumClimateAction>(this->action));
out.append("\n");
out.append("}");
}
bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {

View file

@ -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;

View file

@ -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<climate::ClimateAction>(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();
}

View file

@ -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};

View file

@ -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);
}

View file

@ -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};

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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};