diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index cf527988fe..29f08fbd89 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -9,136 +9,393 @@ static const char *TAG = "bang_bang.climate"; void BangBangClimate::setup() { this->sensor_->add_on_state_callback([this](float state) { this->current_temperature = state; - // control may have changed, recompute - this->compute_state_(); - // current temperature changed, publish state + // required action may have changed, recompute, refresh + this->switch_to_action_(compute_action_()); + // current temperature and possibly action changed, so publish the new state this->publish_state(); }); this->current_temperature = this->sensor_->state; - // restore set points + // restore all climate data, if possible auto restore = this->restore_state_(); if (restore.has_value()) { restore->to_call(this).perform(); } else { - // restore from defaults, change_away handles those for us + // restore from defaults, change_away handles temps for us this->mode = climate::CLIMATE_MODE_AUTO; this->change_away_(false); } + // refresh the climate action based on the restored settings + this->switch_to_action_(compute_action_()); + this->setup_complete_ = true; + this->publish_state(); +} +void BangBangClimate::refresh() { + this->switch_to_mode_(this->mode); + this->switch_to_action_(compute_action_()); + this->switch_to_fan_mode_(this->fan_mode); + this->switch_to_swing_mode_(this->swing_mode); + this->publish_state(); } void BangBangClimate::control(const climate::ClimateCall &call) { if (call.get_mode().has_value()) this->mode = *call.get_mode(); + if (call.get_fan_mode().has_value()) + this->fan_mode = *call.get_fan_mode(); + if (call.get_swing_mode().has_value()) + this->swing_mode = *call.get_swing_mode(); + if (call.get_target_temperature().has_value()) + this->target_temperature = *call.get_target_temperature(); if (call.get_target_temperature_low().has_value()) this->target_temperature_low = *call.get_target_temperature_low(); if (call.get_target_temperature_high().has_value()) this->target_temperature_high = *call.get_target_temperature_high(); - if (call.get_away().has_value()) - this->change_away_(*call.get_away()); - - this->compute_state_(); - this->publish_state(); + if (call.get_away().has_value()) { + // setup_complete_ blocks modifying/resetting the temps immediately after boot + if (this->setup_complete_) { + this->change_away_(*call.get_away()); + } else { + this->away = *call.get_away(); + } + } + // set point validation + if (this->supports_two_points_) { + if (this->target_temperature_low < this->get_traits().get_visual_min_temperature()) + this->target_temperature_low = this->get_traits().get_visual_min_temperature(); + if (this->target_temperature_high > this->get_traits().get_visual_max_temperature()) + this->target_temperature_high = this->get_traits().get_visual_max_temperature(); + if (this->target_temperature_high < this->target_temperature_low) + this->target_temperature_high = this->target_temperature_low; + } else { + if (this->target_temperature < this->get_traits().get_visual_min_temperature()) + this->target_temperature = this->get_traits().get_visual_min_temperature(); + if (this->target_temperature > this->get_traits().get_visual_max_temperature()) + this->target_temperature = this->get_traits().get_visual_max_temperature(); + } + // make any changes happen + refresh(); } climate::ClimateTraits BangBangClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); - traits.set_supports_auto_mode(true); + traits.set_supports_auto_mode(this->supports_auto_); traits.set_supports_cool_mode(this->supports_cool_); + traits.set_supports_dry_mode(this->supports_dry_); + traits.set_supports_fan_only_mode(this->supports_fan_only_); traits.set_supports_heat_mode(this->supports_heat_); - traits.set_supports_two_point_target_temperature(true); + traits.set_supports_fan_mode_on(this->supports_fan_mode_on_); + traits.set_supports_fan_mode_off(this->supports_fan_mode_off_); + traits.set_supports_fan_mode_auto(this->supports_fan_mode_auto_); + traits.set_supports_fan_mode_low(this->supports_fan_mode_low_); + traits.set_supports_fan_mode_medium(this->supports_fan_mode_medium_); + traits.set_supports_fan_mode_high(this->supports_fan_mode_high_); + traits.set_supports_fan_mode_middle(this->supports_fan_mode_middle_); + traits.set_supports_fan_mode_focus(this->supports_fan_mode_focus_); + traits.set_supports_fan_mode_diffuse(this->supports_fan_mode_diffuse_); + traits.set_supports_swing_mode_both(this->supports_swing_mode_both_); + traits.set_supports_swing_mode_horizontal(this->supports_swing_mode_horizontal_); + traits.set_supports_swing_mode_off(this->supports_swing_mode_off_); + traits.set_supports_swing_mode_vertical(this->supports_swing_mode_vertical_); + traits.set_supports_two_point_target_temperature(this->supports_two_points_); 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, switch directly to appropriate action - // - HEAT mode -> HEATING action - // - COOL mode -> COOLING action - // - OFF mode -> OFF action (not IDLE!) - this->switch_to_action_(static_cast(this->mode)); - return; - } - if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || isnan(this->target_temperature_high)) { - // if any control parameters are nan, go to OFF action (not IDLE!) - 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::ClimateAction BangBangClimate::compute_action_() { + climate::ClimateAction target_action = this->action; + if (this->supports_two_points_) { + if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || + isnan(this->target_temperature_high) || isnan(this->hysteresis_)) + // if any control parameters are nan, go to OFF action (not IDLE!) + return climate::CLIMATE_ACTION_OFF; - climate::ClimateAction target_action; - if (too_cold) { - // too cold -> enable heating if possible, else idle - if (this->supports_heat_) - target_action = climate::CLIMATE_ACTION_HEATING; - else - target_action = climate::CLIMATE_ACTION_IDLE; - } else if (too_hot) { - // too hot -> enable cooling if possible, else idle - if (this->supports_cool_) - target_action = climate::CLIMATE_ACTION_COOLING; - else + if (((this->action == climate::CLIMATE_ACTION_FAN) && (this->mode != climate::CLIMATE_MODE_FAN_ONLY)) || + ((this->action == climate::CLIMATE_ACTION_DRYING) && (this->mode != climate::CLIMATE_MODE_DRY))) { target_action = climate::CLIMATE_ACTION_IDLE; + } + + switch (this->mode) { + case climate::CLIMATE_MODE_FAN_ONLY: + if (this->supports_fan_only_) { + if (this->current_temperature > this->target_temperature_high + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_FAN; + else if (this->current_temperature < this->target_temperature_high - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_FAN) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + case climate::CLIMATE_MODE_DRY: + target_action = climate::CLIMATE_ACTION_DRYING; + break; + case climate::CLIMATE_MODE_OFF: + target_action = climate::CLIMATE_ACTION_OFF; + break; + case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_COOL: + case climate::CLIMATE_MODE_HEAT: + if (this->supports_cool_) { + if (this->current_temperature > this->target_temperature_high + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_COOLING; + else if (this->current_temperature < this->target_temperature_high - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_COOLING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + if (this->supports_heat_) { + if (this->current_temperature < this->target_temperature_low - this->hysteresis_) + target_action = climate::CLIMATE_ACTION_HEATING; + else if (this->current_temperature > this->target_temperature_low + this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_HEATING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + default: + break; + } } else { - // neither too hot nor too cold -> in range - if (this->supports_cool_ && this->supports_heat_) { - // if supports both ends, go to idle action + if (isnan(this->current_temperature) || isnan(this->target_temperature) || isnan(this->hysteresis_)) + // if any control parameters are nan, go to OFF action (not IDLE!) + return climate::CLIMATE_ACTION_OFF; + + if (((this->action == climate::CLIMATE_ACTION_FAN) && (this->mode != climate::CLIMATE_MODE_FAN_ONLY)) || + ((this->action == climate::CLIMATE_ACTION_DRYING) && (this->mode != climate::CLIMATE_MODE_DRY))) { target_action = climate::CLIMATE_ACTION_IDLE; - } else { - // else use current mode and don't change (hysteresis) - target_action = this->action; + } + + switch (this->mode) { + case climate::CLIMATE_MODE_FAN_ONLY: + if (this->supports_fan_only_) { + if (this->current_temperature > this->target_temperature + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_FAN; + else if (this->current_temperature < this->target_temperature - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_FAN) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + case climate::CLIMATE_MODE_DRY: + target_action = climate::CLIMATE_ACTION_DRYING; + break; + case climate::CLIMATE_MODE_OFF: + target_action = climate::CLIMATE_ACTION_OFF; + break; + case climate::CLIMATE_MODE_COOL: + if (this->supports_cool_) { + if (this->current_temperature > this->target_temperature + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_COOLING; + else if (this->current_temperature < this->target_temperature - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_COOLING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + case climate::CLIMATE_MODE_HEAT: + if (this->supports_heat_) { + if (this->current_temperature < this->target_temperature - this->hysteresis_) + target_action = climate::CLIMATE_ACTION_HEATING; + else if (this->current_temperature > this->target_temperature + this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_HEATING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + default: + break; } } + // do not switch to an action that isn't enabled per the active climate mode + if ((this->mode == climate::CLIMATE_MODE_COOL) && (target_action == climate::CLIMATE_ACTION_HEATING)) + target_action = climate::CLIMATE_ACTION_IDLE; + if ((this->mode == climate::CLIMATE_MODE_HEAT) && (target_action == climate::CLIMATE_ACTION_COOLING)) + target_action = climate::CLIMATE_ACTION_IDLE; - this->switch_to_action_(target_action); + return target_action; } void BangBangClimate::switch_to_action_(climate::ClimateAction action) { - if (action == this->action) + // setup_complete_ helps us ensure an action is called immediately after boot + if ((action == this->action) && this->setup_complete_) // already in target mode return; - if ((action == climate::CLIMATE_ACTION_OFF && this->action == climate::CLIMATE_ACTION_IDLE) || - (action == climate::CLIMATE_ACTION_IDLE && this->action == climate::CLIMATE_ACTION_OFF)) { + if (((action == climate::CLIMATE_ACTION_OFF && this->action == climate::CLIMATE_ACTION_IDLE) || + (action == climate::CLIMATE_ACTION_IDLE && this->action == climate::CLIMATE_ACTION_OFF)) && + this->setup_complete_) { // switching from OFF to IDLE or vice-versa // these only have visual difference. OFF means user manually disabled, // IDLE means it's in auto mode but value is in target range. this->action = action; - this->publish_state(); return; } - if (this->prev_trigger_ != nullptr) { - this->prev_trigger_->stop(); - this->prev_trigger_ = nullptr; + if (this->prev_action_trigger_ != nullptr) { + this->prev_action_trigger_->stop(); + this->prev_action_trigger_ = nullptr; } - Trigger<> *trig; + Trigger<> *trig = this->idle_action_trigger_; switch (action) { case climate::CLIMATE_ACTION_OFF: case climate::CLIMATE_ACTION_IDLE: - trig = this->idle_trigger_; + // trig = this->idle_action_trigger_; break; case climate::CLIMATE_ACTION_COOLING: - trig = this->cool_trigger_; + trig = this->cool_action_trigger_; break; case climate::CLIMATE_ACTION_HEATING: - trig = this->heat_trigger_; + trig = this->heat_action_trigger_; + break; + case climate::CLIMATE_ACTION_FAN: + trig = this->fan_only_action_trigger_; + break; + case climate::CLIMATE_ACTION_DRYING: + trig = this->dry_action_trigger_; break; default: - trig = nullptr; + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + action = climate::CLIMATE_ACTION_OFF; + // trig = this->idle_action_trigger_; } assert(trig != nullptr); trig->trigger(); this->action = action; - this->prev_trigger_ = trig; - this->publish_state(); + this->prev_action_trigger_ = trig; +} +void BangBangClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((fan_mode == this->prev_fan_mode_) && this->setup_complete_) + // already in target mode + return; + + if (this->prev_fan_mode_trigger_ != nullptr) { + this->prev_fan_mode_trigger_->stop(); + this->prev_fan_mode_trigger_ = nullptr; + } + Trigger<> *trig = this->fan_mode_auto_trigger_; + switch (fan_mode) { + case climate::CLIMATE_FAN_ON: + trig = this->fan_mode_on_trigger_; + break; + case climate::CLIMATE_FAN_OFF: + trig = this->fan_mode_off_trigger_; + break; + case climate::CLIMATE_FAN_AUTO: + // trig = this->fan_mode_auto_trigger_; + break; + case climate::CLIMATE_FAN_LOW: + trig = this->fan_mode_low_trigger_; + break; + case climate::CLIMATE_FAN_MEDIUM: + trig = this->fan_mode_medium_trigger_; + break; + case climate::CLIMATE_FAN_HIGH: + trig = this->fan_mode_high_trigger_; + break; + case climate::CLIMATE_FAN_MIDDLE: + trig = this->fan_mode_middle_trigger_; + break; + case climate::CLIMATE_FAN_FOCUS: + trig = this->fan_mode_focus_trigger_; + break; + case climate::CLIMATE_FAN_DIFFUSE: + trig = this->fan_mode_diffuse_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + fan_mode = climate::CLIMATE_FAN_AUTO; + // trig = this->fan_mode_auto_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + this->fan_mode = fan_mode; + this->prev_fan_mode_ = fan_mode; + this->prev_fan_mode_trigger_ = trig; +} +void BangBangClimate::switch_to_mode_(climate::ClimateMode mode) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((mode == this->prev_mode_) && this->setup_complete_) + // already in target mode + return; + + if (this->prev_mode_trigger_ != nullptr) { + this->prev_mode_trigger_->stop(); + this->prev_mode_trigger_ = nullptr; + } + Trigger<> *trig = this->auto_mode_trigger_; + switch (mode) { + case climate::CLIMATE_MODE_OFF: + trig = this->off_mode_trigger_; + break; + case climate::CLIMATE_MODE_AUTO: + // trig = this->auto_mode_trigger_; + break; + case climate::CLIMATE_MODE_COOL: + trig = this->cool_mode_trigger_; + break; + case climate::CLIMATE_MODE_HEAT: + trig = this->heat_mode_trigger_; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + trig = this->fan_only_mode_trigger_; + break; + case climate::CLIMATE_MODE_DRY: + trig = this->dry_mode_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + mode = climate::CLIMATE_MODE_AUTO; + // trig = this->auto_mode_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + this->mode = mode; + this->prev_mode_ = mode; + this->prev_mode_trigger_ = trig; +} +void BangBangClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mode) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((swing_mode == this->prev_swing_mode_) && this->setup_complete_) + // already in target mode + return; + + if (this->prev_swing_mode_trigger_ != nullptr) { + this->prev_swing_mode_trigger_->stop(); + this->prev_swing_mode_trigger_ = nullptr; + } + Trigger<> *trig = this->swing_mode_off_trigger_; + switch (swing_mode) { + case climate::CLIMATE_SWING_BOTH: + trig = this->swing_mode_both_trigger_; + break; + case climate::CLIMATE_SWING_HORIZONTAL: + trig = this->swing_mode_horizontal_trigger_; + break; + case climate::CLIMATE_SWING_OFF: + // trig = this->swing_mode_off_trigger_; + break; + case climate::CLIMATE_SWING_VERTICAL: + trig = this->swing_mode_vertical_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + swing_mode = climate::CLIMATE_SWING_OFF; + // trig = this->swing_mode_off_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + this->swing_mode = swing_mode; + this->prev_swing_mode_ = swing_mode; + this->prev_swing_mode_trigger_ = trig; } void BangBangClimate::change_away_(bool away) { if (!away) { - this->target_temperature_low = this->normal_config_.default_temperature_low; - this->target_temperature_high = this->normal_config_.default_temperature_high; + if (this->supports_two_points_) { + this->target_temperature_low = this->normal_config_.default_temperature_low; + this->target_temperature_high = this->normal_config_.default_temperature_high; + } else + this->target_temperature = this->normal_config_.default_temperature; } else { - this->target_temperature_low = this->away_config_.default_temperature_low; - this->target_temperature_high = this->away_config_.default_temperature_high; + if (this->supports_two_points_) { + this->target_temperature_low = this->away_config_.default_temperature_low; + this->target_temperature_high = this->away_config_.default_temperature_high; + } else + this->target_temperature = this->away_config_.default_temperature; } this->away = away; } @@ -150,23 +407,142 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa this->away_config_ = away_config; } BangBangClimate::BangBangClimate() - : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {} + : cool_action_trigger_(new Trigger<>()), + cool_mode_trigger_(new Trigger<>()), + dry_action_trigger_(new Trigger<>()), + dry_mode_trigger_(new Trigger<>()), + heat_action_trigger_(new Trigger<>()), + heat_mode_trigger_(new Trigger<>()), + auto_mode_trigger_(new Trigger<>()), + idle_action_trigger_(new Trigger<>()), + off_mode_trigger_(new Trigger<>()), + fan_only_action_trigger_(new Trigger<>()), + fan_only_mode_trigger_(new Trigger<>()), + fan_mode_on_trigger_(new Trigger<>()), + fan_mode_off_trigger_(new Trigger<>()), + fan_mode_auto_trigger_(new Trigger<>()), + fan_mode_low_trigger_(new Trigger<>()), + fan_mode_medium_trigger_(new Trigger<>()), + fan_mode_high_trigger_(new Trigger<>()), + fan_mode_middle_trigger_(new Trigger<>()), + fan_mode_focus_trigger_(new Trigger<>()), + fan_mode_diffuse_trigger_(new Trigger<>()), + swing_mode_both_trigger_(new Trigger<>()), + swing_mode_off_trigger_(new Trigger<>()), + swing_mode_horizontal_trigger_(new Trigger<>()), + swing_mode_vertical_trigger_(new Trigger<>()) {} +void BangBangClimate::set_hysteresis(float hysteresis) { this->hysteresis_ = hysteresis; } void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } -Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; } -Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; } +void BangBangClimate::set_supports_auto(bool supports_auto) { this->supports_auto_ = supports_auto; } void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } -Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; } +void BangBangClimate::set_supports_dry(bool supports_dry) { this->supports_dry_ = supports_dry; } +void BangBangClimate::set_supports_fan_only(bool supports_fan_only) { this->supports_fan_only_ = supports_fan_only; } void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } +void BangBangClimate::set_supports_fan_mode_on(bool supports_fan_mode_on) { + this->supports_fan_mode_on_ = supports_fan_mode_on; +} +void BangBangClimate::set_supports_fan_mode_off(bool supports_fan_mode_off) { + this->supports_fan_mode_off_ = supports_fan_mode_off; +} +void BangBangClimate::set_supports_fan_mode_auto(bool supports_fan_mode_auto) { + this->supports_fan_mode_auto_ = supports_fan_mode_auto; +} +void BangBangClimate::set_supports_fan_mode_low(bool supports_fan_mode_low) { + this->supports_fan_mode_low_ = supports_fan_mode_low; +} +void BangBangClimate::set_supports_fan_mode_medium(bool supports_fan_mode_medium) { + this->supports_fan_mode_medium_ = supports_fan_mode_medium; +} +void BangBangClimate::set_supports_fan_mode_high(bool supports_fan_mode_high) { + this->supports_fan_mode_high_ = supports_fan_mode_high; +} +void BangBangClimate::set_supports_fan_mode_middle(bool supports_fan_mode_middle) { + this->supports_fan_mode_middle_ = supports_fan_mode_middle; +} +void BangBangClimate::set_supports_fan_mode_focus(bool supports_fan_mode_focus) { + this->supports_fan_mode_focus_ = supports_fan_mode_focus; +} +void BangBangClimate::set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse) { + this->supports_fan_mode_diffuse_ = supports_fan_mode_diffuse; +} +void BangBangClimate::set_supports_swing_mode_both(bool supports_swing_mode_both) { + this->supports_swing_mode_both_ = supports_swing_mode_both; +} +void BangBangClimate::set_supports_swing_mode_off(bool supports_swing_mode_off) { + this->supports_swing_mode_off_ = supports_swing_mode_off; +} +void BangBangClimate::set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal) { + this->supports_swing_mode_horizontal_ = supports_swing_mode_horizontal; +} +void BangBangClimate::set_supports_swing_mode_vertical(bool supports_swing_mode_vertical) { + this->supports_swing_mode_vertical_ = supports_swing_mode_vertical; +} +void BangBangClimate::set_supports_two_points(bool supports_two_points) { + this->supports_two_points_ = supports_two_points; +} +Trigger<> *BangBangClimate::get_cool_action_trigger() const { return this->cool_action_trigger_; } +Trigger<> *BangBangClimate::get_dry_action_trigger() const { return this->dry_action_trigger_; } +Trigger<> *BangBangClimate::get_fan_only_action_trigger() const { return this->fan_only_action_trigger_; } +Trigger<> *BangBangClimate::get_heat_action_trigger() const { return this->heat_action_trigger_; } +Trigger<> *BangBangClimate::get_idle_action_trigger() const { return this->idle_action_trigger_; } +Trigger<> *BangBangClimate::get_auto_mode_trigger() const { return this->auto_mode_trigger_; } +Trigger<> *BangBangClimate::get_cool_mode_trigger() const { return this->cool_mode_trigger_; } +Trigger<> *BangBangClimate::get_dry_mode_trigger() const { return this->dry_mode_trigger_; } +Trigger<> *BangBangClimate::get_fan_only_mode_trigger() const { return this->fan_only_mode_trigger_; } +Trigger<> *BangBangClimate::get_heat_mode_trigger() const { return this->heat_mode_trigger_; } +Trigger<> *BangBangClimate::get_off_mode_trigger() const { return this->off_mode_trigger_; } +Trigger<> *BangBangClimate::get_fan_mode_on_trigger() const { return this->fan_mode_on_trigger_; } +Trigger<> *BangBangClimate::get_fan_mode_off_trigger() const { return this->fan_mode_off_trigger_; } +Trigger<> *BangBangClimate::get_fan_mode_auto_trigger() const { return this->fan_mode_auto_trigger_; } +Trigger<> *BangBangClimate::get_fan_mode_low_trigger() const { return this->fan_mode_low_trigger_; } +Trigger<> *BangBangClimate::get_fan_mode_medium_trigger() const { return this->fan_mode_medium_trigger_; } +Trigger<> *BangBangClimate::get_fan_mode_high_trigger() const { return this->fan_mode_high_trigger_; } +Trigger<> *BangBangClimate::get_fan_mode_middle_trigger() const { return this->fan_mode_middle_trigger_; } +Trigger<> *BangBangClimate::get_fan_mode_focus_trigger() const { return this->fan_mode_focus_trigger_; } +Trigger<> *BangBangClimate::get_fan_mode_diffuse_trigger() const { return this->fan_mode_diffuse_trigger_; } +Trigger<> *BangBangClimate::get_swing_mode_both_trigger() const { return this->swing_mode_both_trigger_; } +Trigger<> *BangBangClimate::get_swing_mode_off_trigger() const { return this->swing_mode_off_trigger_; } +Trigger<> *BangBangClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; } +Trigger<> *BangBangClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; } void BangBangClimate::dump_config() { LOG_CLIMATE("", "Bang Bang Climate", this); - ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); + if (this->supports_heat_) + ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low); + if ((this->supports_cool_) || (this->supports_fan_only_)) + ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high); + ESP_LOGCONFIG(TAG, " Hysteresis: %.1f°C", this->hysteresis_); + ESP_LOGCONFIG(TAG, " Supports AUTO: %s", YESNO(this->supports_auto_)); ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); + ESP_LOGCONFIG(TAG, " Supports DRY: %s", YESNO(this->supports_dry_)); + ESP_LOGCONFIG(TAG, " Supports FAN_ONLY: %s", YESNO(this->supports_fan_only_)); + ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE ON: %s", YESNO(this->supports_fan_mode_on_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE OFF: %s", YESNO(this->supports_fan_mode_off_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE AUTO: %s", YESNO(this->supports_fan_mode_auto_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE LOW: %s", YESNO(this->supports_fan_mode_low_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE MEDIUM: %s", YESNO(this->supports_fan_mode_medium_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE HIGH: %s", YESNO(this->supports_fan_mode_high_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE MIDDLE: %s", YESNO(this->supports_fan_mode_middle_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE FOCUS: %s", YESNO(this->supports_fan_mode_focus_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE DIFFUSE: %s", YESNO(this->supports_fan_mode_diffuse_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE BOTH: %s", YESNO(this->supports_swing_mode_both_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE OFF: %s", YESNO(this->supports_swing_mode_off_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE HORIZONTAL: %s", YESNO(this->supports_swing_mode_horizontal_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE VERTICAL: %s", YESNO(this->supports_swing_mode_vertical_)); + ESP_LOGCONFIG(TAG, " Supports TWO SET POINTS: %s", YESNO(this->supports_two_points_)); ESP_LOGCONFIG(TAG, " Supports AWAY mode: %s", YESNO(this->supports_away_)); - ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low); - ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high); + if (this->supports_away_) { + if (this->supports_heat_) + ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C", this->away_config_.default_temperature_low); + if ((this->supports_cool_) || (this->supports_fan_only_)) + ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C", + this->away_config_.default_temperature_high); + } } BangBangClimateTargetTempConfig::BangBangClimateTargetTempConfig() = default; +BangBangClimateTargetTempConfig::BangBangClimateTargetTempConfig(float default_temperature) + : default_temperature(default_temperature) {} BangBangClimateTargetTempConfig::BangBangClimateTargetTempConfig(float default_temperature_low, float default_temperature_high) : default_temperature_low(default_temperature_low), default_temperature_high(default_temperature_high) {} diff --git a/esphome/components/bang_bang/bang_bang_climate.h b/esphome/components/bang_bang/bang_bang_climate.h index 84bcd51f34..fd2b0ce43a 100644 --- a/esphome/components/bang_bang/bang_bang_climate.h +++ b/esphome/components/bang_bang/bang_bang_climate.h @@ -11,10 +11,13 @@ namespace bang_bang { struct BangBangClimateTargetTempConfig { public: BangBangClimateTargetTempConfig(); + BangBangClimateTargetTempConfig(float default_temperature); BangBangClimateTargetTempConfig(float default_temperature_low, float default_temperature_high); + float default_temperature{NAN}; float default_temperature_low{NAN}; float default_temperature_high{NAN}; + float hysteresis{NAN}; }; class BangBangClimate : public climate::Climate, public Component { @@ -23,62 +26,243 @@ class BangBangClimate : public climate::Climate, public Component { void setup() override; void dump_config() override; + void set_hysteresis(float hysteresis); void set_sensor(sensor::Sensor *sensor); - Trigger<> *get_idle_trigger() const; - Trigger<> *get_cool_trigger() const; + void set_supports_auto(bool supports_auto); void set_supports_cool(bool supports_cool); - Trigger<> *get_heat_trigger() const; + void set_supports_dry(bool supports_dry); + void set_supports_fan_only(bool supports_fan_only); void set_supports_heat(bool supports_heat); + void set_supports_fan_mode_on(bool supports_fan_mode_on); + void set_supports_fan_mode_off(bool supports_fan_mode_off); + void set_supports_fan_mode_auto(bool supports_fan_mode_auto); + void set_supports_fan_mode_low(bool supports_fan_mode_low); + void set_supports_fan_mode_medium(bool supports_fan_mode_medium); + void set_supports_fan_mode_high(bool supports_fan_mode_high); + void set_supports_fan_mode_middle(bool supports_fan_mode_middle); + void set_supports_fan_mode_focus(bool supports_fan_mode_focus); + void set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse); + void set_supports_swing_mode_both(bool supports_swing_mode_both); + void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal); + void set_supports_swing_mode_off(bool supports_swing_mode_off); + void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical); + void set_supports_two_points(bool supports_two_points); + void set_normal_config(const BangBangClimateTargetTempConfig &normal_config); void set_away_config(const BangBangClimateTargetTempConfig &away_config); + Trigger<> *get_cool_action_trigger() const; + Trigger<> *get_dry_action_trigger() const; + Trigger<> *get_fan_only_action_trigger() const; + Trigger<> *get_heat_action_trigger() const; + Trigger<> *get_idle_action_trigger() const; + Trigger<> *get_auto_mode_trigger() const; + Trigger<> *get_cool_mode_trigger() const; + Trigger<> *get_dry_mode_trigger() const; + Trigger<> *get_fan_only_mode_trigger() const; + Trigger<> *get_heat_mode_trigger() const; + Trigger<> *get_off_mode_trigger() const; + Trigger<> *get_fan_mode_on_trigger() const; + Trigger<> *get_fan_mode_off_trigger() const; + Trigger<> *get_fan_mode_auto_trigger() const; + Trigger<> *get_fan_mode_low_trigger() const; + Trigger<> *get_fan_mode_medium_trigger() const; + Trigger<> *get_fan_mode_high_trigger() const; + Trigger<> *get_fan_mode_middle_trigger() const; + Trigger<> *get_fan_mode_focus_trigger() const; + Trigger<> *get_fan_mode_diffuse_trigger() const; + Trigger<> *get_swing_mode_both_trigger() const; + Trigger<> *get_swing_mode_horizontal_trigger() const; + Trigger<> *get_swing_mode_off_trigger() const; + Trigger<> *get_swing_mode_vertical_trigger() const; + /// Call triggers based on updated climate states (modes/actions) + void refresh(); + protected: /// Override control to change settings of the climate device. void control(const climate::ClimateCall &call) override; + /// Change the away setting, will reset target temperatures to defaults. void change_away_(bool away); + /// Return the traits of this controller. climate::ClimateTraits traits() override; - /// Re-compute the state of this climate controller. - void compute_state_(); + /// Re-compute the required action of this climate controller. + climate::ClimateAction compute_action_(); + + /// Switch the climate device to the given climate action. + void switch_to_action_(climate::ClimateAction action); + + /// Switch the climate device to the given climate fan mode. + void switch_to_fan_mode_(climate::ClimateFanMode fan_mode); /// Switch the climate device to the given climate mode. - void switch_to_action_(climate::ClimateAction action); + void switch_to_mode_(climate::ClimateMode mode); + + /// Switch the climate device to the given climate swing mode. + void switch_to_swing_mode_(climate::ClimateSwingMode swing_mode); /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; - /** The trigger to call when the controller should switch to idle mode. - * - * In idle mode, the controller is assumed to have both heating and cooling disabled. - */ - Trigger<> *idle_trigger_; - /** The trigger to call when the controller should switch to cooling mode. - */ - Trigger<> *cool_trigger_; - /** Whether the controller supports cooling. - * - * A false value for this attribute means that the controller has no cooling action - * (for example a thermostat, where only heating and not-heating is possible). - */ - bool supports_cool_{false}; - /** The trigger to call when the controller should switch to heating mode. - * - * A null value for this attribute means that the controller has no heating action - * For example window blinds, where only cooling (blinds closed) and not-cooling - * (blinds open) is possible. - */ - Trigger<> *heat_trigger_{nullptr}; - bool supports_heat_{false}; - /** A reference to the trigger that was previously active. - * - * This is so that the previous trigger can be stopped before enabling a new one. - */ - Trigger<> *prev_trigger_{nullptr}; - BangBangClimateTargetTempConfig normal_config_{}; + /// Whether the controller supports auto/cooling/drying/fanning/heating. + /// + /// A false value for any given attribute means that the controller has no such action + /// (for example a thermostat, where only heating and not-heating is possible). + bool supports_auto_{false}; + bool supports_cool_{false}; + bool supports_dry_{false}; + bool supports_fan_only_{false}; + bool supports_heat_{false}; + + /// Whether the controller supports turning on or off just the fan. + /// + /// A false value for either attribute means that the controller has no fan on/off action + /// (for example a thermostat, where independent control of the fan is not possible). + bool supports_fan_mode_on_{false}; + bool supports_fan_mode_off_{false}; + + /// Whether the controller supports fan auto mode. + /// + /// A false value for this attribute means that the controller has no fan-auto action + /// (for example a thermostat, where independent control of the fan is not possible). + bool supports_fan_mode_auto_{false}; + + /// Whether the controller supports various fan speeds and/or positions. + /// + /// A false value for any given attribute means that the controller has no such fan action. + bool supports_fan_mode_low_{false}; + bool supports_fan_mode_medium_{false}; + bool supports_fan_mode_high_{false}; + bool supports_fan_mode_middle_{false}; + bool supports_fan_mode_focus_{false}; + bool supports_fan_mode_diffuse_{false}; + + /// Whether the controller supports various swing modes. + /// + /// A false value for any given attribute means that the controller has no such swing mode. + bool supports_swing_mode_both_{false}; + bool supports_swing_mode_off_{false}; + bool supports_swing_mode_horizontal_{false}; + bool supports_swing_mode_vertical_{false}; + + /// Whether the controller supports two set points + /// + /// A false value means that the controller has no such support. + bool supports_two_points_{false}; + + /// Whether the controller supports an "away" mode + /// + /// A false value means that the controller has no such mode. bool supports_away_{false}; + + /// The trigger to call when the controller should switch to cooling action/mode. + /// + /// A null value for this attribute means that the controller has no cooling action + /// For example electric heat, where only heating (power on) and not-heating + /// (power off) is possible. + Trigger<> *cool_action_trigger_{nullptr}; + Trigger<> *cool_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to dry (dehumidification) mode. + /// + /// In dry mode, the controller is assumed to have both heating and cooling disabled, + /// although the system may use its cooling mechanism to achieve drying. + Trigger<> *dry_action_trigger_{nullptr}; + Trigger<> *dry_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to heating action/mode. + /// + /// A null value for this attribute means that the controller has no heating action + /// For example window blinds, where only cooling (blinds closed) and not-cooling + /// (blinds open) is possible. + Trigger<> *heat_action_trigger_{nullptr}; + Trigger<> *heat_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to auto mode. + /// + /// In auto mode, the controller will enable heating/cooling as necessary and switch + /// to idle when the temperature is within the thresholds/set points. + Trigger<> *auto_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to idle action/off mode. + /// + /// In these actions/modes, the controller is assumed to have both heating and cooling disabled. + Trigger<> *idle_action_trigger_{nullptr}; + Trigger<> *off_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to fan-only action/mode. + /// + /// In fan-only mode, the controller is assumed to have both heating and cooling disabled. + /// The system should activate the fan only. + Trigger<> *fan_only_action_trigger_{nullptr}; + Trigger<> *fan_only_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch on the fan. + Trigger<> *fan_mode_on_trigger_{nullptr}; + + /// The trigger to call when the controller should switch off the fan. + Trigger<> *fan_mode_off_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "auto" mode. + Trigger<> *fan_mode_auto_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "low" speed. + Trigger<> *fan_mode_low_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "medium" speed. + Trigger<> *fan_mode_medium_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "high" speed. + Trigger<> *fan_mode_high_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "middle" position. + Trigger<> *fan_mode_middle_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "focus" position. + Trigger<> *fan_mode_focus_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "diffuse" position. + Trigger<> *fan_mode_diffuse_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "both". + Trigger<> *swing_mode_both_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "off". + Trigger<> *swing_mode_off_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "horizontal". + Trigger<> *swing_mode_horizontal_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "vertical". + Trigger<> *swing_mode_vertical_trigger_{nullptr}; + + /// A reference to the trigger that was previously active. + /// + /// This is so that the previous trigger can be stopped before enabling a new one + /// for each climate category (mode, action, fan_mode, swing_mode). + Trigger<> *prev_action_trigger_{nullptr}; + Trigger<> *prev_fan_mode_trigger_{nullptr}; + Trigger<> *prev_mode_trigger_{nullptr}; + Trigger<> *prev_swing_mode_trigger_{nullptr}; + + /// Store previously-known states + /// + /// These are used to determine when a trigger/action needs to be called + climate::ClimateFanMode prev_fan_mode_{climate::CLIMATE_FAN_ON}; + climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; + climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; + + /// Temperature data for normal/home and away modes + BangBangClimateTargetTempConfig normal_config_{}; BangBangClimateTargetTempConfig away_config_{}; + + /// Hysteresis value used for computing climate actions + float hysteresis_{0}; + + /// setup_complete_ blocks modifying/resetting the temps immediately after boot + bool setup_complete_{false}; }; } // namespace bang_bang diff --git a/esphome/components/bang_bang/climate.py b/esphome/components/bang_bang/climate.py index 4ef811c55d..75ec58404b 100644 --- a/esphome/components/bang_bang/climate.py +++ b/esphome/components/bang_bang/climate.py @@ -2,27 +2,107 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import climate, sensor -from esphome.const import CONF_AWAY_CONFIG, CONF_COOL_ACTION, \ - CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION, \ - CONF_ID, CONF_IDLE_ACTION, CONF_SENSOR +from esphome.const import CONF_AUTO_MODE, CONF_AWAY_CONFIG, CONF_COOL_ACTION, CONF_COOL_MODE, \ + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_DRY_ACTION, \ + CONF_DRY_MODE, CONF_FAN_MODE_ON_ACTION, CONF_FAN_MODE_OFF_ACTION, CONF_FAN_MODE_AUTO_ACTION, \ + CONF_FAN_MODE_LOW_ACTION, CONF_FAN_MODE_MEDIUM_ACTION, CONF_FAN_MODE_HIGH_ACTION, \ + CONF_FAN_MODE_MIDDLE_ACTION, CONF_FAN_MODE_FOCUS_ACTION, CONF_FAN_MODE_DIFFUSE_ACTION, \ + CONF_FAN_ONLY_ACTION, CONF_FAN_ONLY_MODE, CONF_HEAT_ACTION, CONF_HEAT_MODE, CONF_HYSTERESIS, \ + CONF_ID, CONF_IDLE_ACTION, CONF_OFF_MODE, CONF_SENSOR, CONF_SWING_BOTH_ACTION, \ + CONF_SWING_HORIZONTAL_ACTION, CONF_SWING_OFF_ACTION, CONF_SWING_VERTICAL_ACTION bang_bang_ns = cg.esphome_ns.namespace('bang_bang') BangBangClimate = bang_bang_ns.class_('BangBangClimate', climate.Climate, cg.Component) BangBangClimateTargetTempConfig = bang_bang_ns.struct('BangBangClimateTargetTempConfig') + +def validate_bangbang(config): + # verify corresponding climate action action exists for any defined climate mode action + if CONF_COOL_MODE in config and CONF_COOL_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_COOL_ACTION, CONF_COOL_MODE)) + if CONF_DRY_MODE in config and CONF_DRY_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_DRY_ACTION, CONF_DRY_MODE)) + if CONF_FAN_ONLY_MODE in config and CONF_FAN_ONLY_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_FAN_ONLY_ACTION, + CONF_FAN_ONLY_MODE)) + if CONF_HEAT_MODE in config and CONF_HEAT_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_HEAT_ACTION, CONF_HEAT_MODE)) + # verify corresponding default target temperature exists when a given climate action exists + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH not in config and (CONF_COOL_ACTION in config + or CONF_FAN_ONLY_ACTION in config): + raise cv.Invalid("{} must be defined when using {} or {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW not in config and CONF_HEAT_ACTION in config: + raise cv.Invalid("{} must be defined when using {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + # if a given climate action is NOT defined, it should not have a default target temperature + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config and (CONF_COOL_ACTION not in config + and CONF_FAN_ONLY_ACTION not in config): + raise cv.Invalid("{} is defined with no {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config and CONF_HEAT_ACTION not in config: + raise cv.Invalid("{} is defined with no {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + + if CONF_AWAY_CONFIG in config: + away = config[CONF_AWAY_CONFIG] + # verify corresponding default target temperature exists when a given climate action exists + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH not in away and (CONF_COOL_ACTION in config or + CONF_FAN_ONLY_ACTION in config): + raise cv.Invalid("{} must be defined in away configuration when using {} or {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW not in away and CONF_HEAT_ACTION in config: + raise cv.Invalid("{} must be defined in away configuration when using {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + # if a given climate action is NOT defined, it should not have a default target temperature + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in away and (CONF_COOL_ACTION not in config and + CONF_FAN_ONLY_ACTION not in config): + raise cv.Invalid("{} is defined in away configuration with no {} or {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in away and CONF_HEAT_ACTION not in config: + raise cv.Invalid("{} is defined in away configuration with no {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + + return config + + CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(BangBangClimate), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), - cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, - cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_COOL_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_DRY_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_ONLY_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_HEAT_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_AUTO_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_COOL_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_DRY_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_ONLY_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_HEAT_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_OFF_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_ON_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_OFF_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_AUTO_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_LOW_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_MEDIUM_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_HIGH_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_MIDDLE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_FOCUS_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_DIFFUSE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_BOTH_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_HORIZONTAL_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_OFF_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_VERTICAL_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, + cv.Optional(CONF_HYSTERESIS, default=0.5): cv.temperature, cv.Optional(CONF_AWAY_CONFIG): cv.Schema({ - cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, - cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, }), -}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_HEAT_ACTION)) +}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_DRY_ACTION, + CONF_FAN_ONLY_ACTION, CONF_HEAT_ACTION), + validate_bangbang) def to_code(config): @@ -30,28 +110,145 @@ def to_code(config): yield cg.register_component(var, config) yield climate.register_climate(var, config) + auto_mode_available = CONF_HEAT_ACTION in config and CONF_COOL_ACTION in config + two_points_available = CONF_HEAT_ACTION in config and (CONF_COOL_ACTION in config or + CONF_FAN_ONLY_ACTION in config) + sens = yield cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) + cg.add(var.set_hysteresis(config[CONF_HYSTERESIS])) - normal_config = BangBangClimateTargetTempConfig( - config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], - config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] - ) + if two_points_available is True: + cg.add(var.set_supports_two_points(True)) + normal_config = BangBangClimateTargetTempConfig( + config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], + config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config: + cg.add(var.set_supports_two_points(False)) + normal_config = BangBangClimateTargetTempConfig( + config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config: + cg.add(var.set_supports_two_points(False)) + normal_config = BangBangClimateTargetTempConfig( + config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] + ) cg.add(var.set_normal_config(normal_config)) - yield automation.build_automation(var.get_idle_trigger(), [], config[CONF_IDLE_ACTION]) + yield automation.build_automation(var.get_idle_action_trigger(), [], + config[CONF_IDLE_ACTION]) + + if auto_mode_available is True: + cg.add(var.set_supports_auto(True)) + else: + cg.add(var.set_supports_auto(False)) if CONF_COOL_ACTION in config: - yield automation.build_automation(var.get_cool_trigger(), [], config[CONF_COOL_ACTION]) + yield automation.build_automation(var.get_cool_action_trigger(), [], + config[CONF_COOL_ACTION]) cg.add(var.set_supports_cool(True)) + if CONF_DRY_ACTION in config: + yield automation.build_automation(var.get_dry_action_trigger(), [], + config[CONF_DRY_ACTION]) + cg.add(var.set_supports_dry(True)) + if CONF_FAN_ONLY_ACTION in config: + yield automation.build_automation(var.get_fan_only_action_trigger(), [], + config[CONF_FAN_ONLY_ACTION]) + cg.add(var.set_supports_fan_only(True)) if CONF_HEAT_ACTION in config: - yield automation.build_automation(var.get_heat_trigger(), [], config[CONF_HEAT_ACTION]) + yield automation.build_automation(var.get_heat_action_trigger(), [], + config[CONF_HEAT_ACTION]) cg.add(var.set_supports_heat(True)) + if CONF_AUTO_MODE in config: + yield automation.build_automation(var.get_auto_mode_trigger(), [], + config[CONF_AUTO_MODE]) + if CONF_COOL_MODE in config: + yield automation.build_automation(var.get_cool_mode_trigger(), [], + config[CONF_COOL_MODE]) + cg.add(var.set_supports_cool(True)) + if CONF_DRY_MODE in config: + yield automation.build_automation(var.get_dry_mode_trigger(), [], + config[CONF_DRY_MODE]) + cg.add(var.set_supports_dry(True)) + if CONF_FAN_ONLY_MODE in config: + yield automation.build_automation(var.get_fan_only_mode_trigger(), [], + config[CONF_FAN_ONLY_MODE]) + cg.add(var.set_supports_fan_only(True)) + if CONF_HEAT_MODE in config: + yield automation.build_automation(var.get_heat_mode_trigger(), [], + config[CONF_HEAT_MODE]) + cg.add(var.set_supports_heat(True)) + if CONF_OFF_MODE in config: + yield automation.build_automation(var.get_off_mode_trigger(), [], + config[CONF_OFF_MODE]) + if CONF_FAN_MODE_ON_ACTION in config: + yield automation.build_automation(var.get_fan_mode_on_trigger(), [], + config[CONF_FAN_MODE_ON_ACTION]) + cg.add(var.set_supports_fan_mode_on(True)) + if CONF_FAN_MODE_OFF_ACTION in config: + yield automation.build_automation(var.get_fan_mode_off_trigger(), [], + config[CONF_FAN_MODE_OFF_ACTION]) + cg.add(var.set_supports_fan_mode_off(True)) + if CONF_FAN_MODE_AUTO_ACTION in config: + yield automation.build_automation(var.get_fan_mode_auto_trigger(), [], + config[CONF_FAN_MODE_AUTO_ACTION]) + cg.add(var.set_supports_fan_mode_auto(True)) + if CONF_FAN_MODE_LOW_ACTION in config: + yield automation.build_automation(var.get_fan_mode_low_trigger(), [], + config[CONF_FAN_MODE_LOW_ACTION]) + cg.add(var.set_supports_fan_mode_low(True)) + if CONF_FAN_MODE_MEDIUM_ACTION in config: + yield automation.build_automation(var.get_fan_mode_medium_trigger(), [], + config[CONF_FAN_MODE_MEDIUM_ACTION]) + cg.add(var.set_supports_fan_mode_medium(True)) + if CONF_FAN_MODE_HIGH_ACTION in config: + yield automation.build_automation(var.get_fan_mode_high_trigger(), [], + config[CONF_FAN_MODE_HIGH_ACTION]) + cg.add(var.set_supports_fan_mode_high(True)) + if CONF_FAN_MODE_MIDDLE_ACTION in config: + yield automation.build_automation(var.get_fan_mode_middle_trigger(), [], + config[CONF_FAN_MODE_MIDDLE_ACTION]) + cg.add(var.set_supports_fan_mode_middle(True)) + if CONF_FAN_MODE_FOCUS_ACTION in config: + yield automation.build_automation(var.get_fan_mode_focus_trigger(), [], + config[CONF_FAN_MODE_FOCUS_ACTION]) + cg.add(var.set_supports_fan_mode_focus(True)) + if CONF_FAN_MODE_DIFFUSE_ACTION in config: + yield automation.build_automation(var.get_fan_mode_diffuse_trigger(), [], + config[CONF_FAN_MODE_DIFFUSE_ACTION]) + cg.add(var.set_supports_fan_mode_diffuse(True)) + if CONF_SWING_BOTH_ACTION in config: + yield automation.build_automation(var.get_swing_mode_both_trigger(), [], + config[CONF_SWING_BOTH_ACTION]) + cg.add(var.set_supports_swing_mode_both(True)) + if CONF_SWING_HORIZONTAL_ACTION in config: + yield automation.build_automation(var.get_swing_mode_horizontal_trigger(), [], + config[CONF_SWING_HORIZONTAL_ACTION]) + cg.add(var.set_supports_swing_mode_horizontal(True)) + if CONF_SWING_OFF_ACTION in config: + yield automation.build_automation(var.get_swing_mode_off_trigger(), [], + config[CONF_SWING_OFF_ACTION]) + cg.add(var.set_supports_swing_mode_off(True)) + if CONF_SWING_VERTICAL_ACTION in config: + yield automation.build_automation(var.get_swing_mode_vertical_trigger(), [], + config[CONF_SWING_VERTICAL_ACTION]) + cg.add(var.set_supports_swing_mode_vertical(True)) if CONF_AWAY_CONFIG in config: away = config[CONF_AWAY_CONFIG] - away_config = BangBangClimateTargetTempConfig( - away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], - away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] - ) + + if two_points_available is True: + away_config = BangBangClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], + away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in away: + away_config = BangBangClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in away: + away_config = BangBangClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] + ) cg.add(var.set_away_config(away_config)) diff --git a/esphome/const.py b/esphome/const.py index 4226e25afa..9626a2e00a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -44,6 +44,7 @@ CONF_ASSUMED_STATE = 'assumed_state' CONF_AT = 'at' CONF_ATTENUATION = 'attenuation' CONF_AUTH = 'auth' +CONF_AUTO_MODE = 'auto_mode' CONF_AUTOMATION_ID = 'automation_id' CONF_AVAILABILITY = 'availability' CONF_AWAY = 'away' @@ -101,6 +102,7 @@ CONF_CONDITION = 'condition' CONF_CONDITION_ID = 'condition_id' CONF_CONDUCTIVITY = 'conductivity' CONF_COOL_ACTION = 'cool_action' +CONF_COOL_MODE = 'cool_mode' CONF_COUNT_MODE = 'count_mode' CONF_CRON = 'cron' CONF_CS_PIN = 'cs_pin' @@ -139,6 +141,8 @@ CONF_DIV_RATIO = 'div_ratio' CONF_DNS1 = 'dns1' CONF_DNS2 = 'dns2' CONF_DOMAIN = 'domain' +CONF_DRY_ACTION = 'dry_action' +CONF_DRY_MODE = 'dry_mode' CONF_DUMP = 'dump' CONF_DURATION = 'duration' CONF_ECHO_PIN = 'echo_pin' @@ -158,6 +162,17 @@ CONF_EXTERNAL_VCC = 'external_vcc' CONF_FALLING_EDGE = 'falling_edge' CONF_FAMILY = 'family' CONF_FAN_MODE = 'fan_mode' +CONF_FAN_MODE_AUTO_ACTION = 'fan_mode_auto_action' +CONF_FAN_MODE_DIFFUSE_ACTION = 'fan_mode_diffuse_action' +CONF_FAN_MODE_FOCUS_ACTION = 'fan_mode_focus_action' +CONF_FAN_MODE_HIGH_ACTION = 'fan_mode_high_action' +CONF_FAN_MODE_LOW_ACTION = 'fan_mode_low_action' +CONF_FAN_MODE_MEDIUM_ACTION = 'fan_mode_medium_action' +CONF_FAN_MODE_MIDDLE_ACTION = 'fan_mode_middle_action' +CONF_FAN_MODE_OFF_ACTION = 'fan_mode_off_action' +CONF_FAN_MODE_ON_ACTION = 'fan_mode_on_action' +CONF_FAN_ONLY_ACTION = 'fan_only_action' +CONF_FAN_ONLY_MODE = 'fan_only_mode' CONF_FAST_CONNECT = 'fast_connect' CONF_FILE = 'file' CONF_FILTER = 'filter' @@ -182,6 +197,7 @@ CONF_GROUP = 'group' CONF_HARDWARE_UART = 'hardware_uart' CONF_HEARTBEAT = 'heartbeat' CONF_HEAT_ACTION = 'heat_action' +CONF_HEAT_MODE = 'heat_mode' CONF_HEATER = 'heater' CONF_HIDDEN = 'hidden' CONF_HIGH = 'high' @@ -189,6 +205,7 @@ CONF_HIGH_VOLTAGE_REFERENCE = 'high_voltage_reference' CONF_HOUR = 'hour' CONF_HOURS = 'hours' CONF_HUMIDITY = 'humidity' +CONF_HYSTERESIS = "hysteresis" CONF_I2C = 'i2c' CONF_I2C_ID = 'i2c_id' CONF_ICON = 'icon' @@ -284,6 +301,7 @@ CONF_NUM_CHANNELS = 'num_channels' CONF_NUM_CHIPS = 'num_chips' CONF_NUM_LEDS = 'num_leds' CONF_NUMBER = 'number' +CONF_OFF_MODE = 'off_mode' CONF_OFFSET = 'offset' CONF_ON = 'on' CONF_ON_BLE_ADVERTISE = 'on_ble_advertise' @@ -448,7 +466,11 @@ CONF_STOP_ACTION = 'stop_action' CONF_SUBNET = 'subnet' CONF_SUPPORTS_COOL = 'supports_cool' CONF_SUPPORTS_HEAT = 'supports_heat' +CONF_SWING_BOTH_ACTION = 'swing_both_action' +CONF_SWING_HORIZONTAL_ACTION = 'swing_horizontal_action' CONF_SWING_MODE = 'swing_mode' +CONF_SWING_OFF_ACTION = 'swing_off_action' +CONF_SWING_VERTICAL_ACTION = 'swing_vertical_action' CONF_SWITCHES = 'switches' CONF_SYNC = 'sync' CONF_TABLET = 'tablet' diff --git a/tests/test3.yaml b/tests/test3.yaml index 0344aed6de..53bbdf023c 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -615,6 +615,49 @@ climate: - switch.turn_on: gpio_switch2 heat_action: - switch.turn_on: gpio_switch1 + dry_action: + - switch.turn_on: gpio_switch2 + fan_only_action: + - switch.turn_on: gpio_switch1 + auto_mode: + - switch.turn_on: gpio_switch2 + off_mode: + - switch.turn_on: gpio_switch1 + heat_mode: + - switch.turn_on: gpio_switch2 + cool_mode: + - switch.turn_on: gpio_switch1 + dry_mode: + - switch.turn_on: gpio_switch2 + fan_only_mode: + - switch.turn_on: gpio_switch1 + fan_mode_auto_action: + - switch.turn_on: gpio_switch2 + fan_mode_on_action: + - switch.turn_on: gpio_switch1 + fan_mode_off_action: + - switch.turn_on: gpio_switch2 + fan_mode_low_action: + - switch.turn_on: gpio_switch1 + fan_mode_medium_action: + - switch.turn_on: gpio_switch2 + fan_mode_high_action: + - switch.turn_on: gpio_switch1 + fan_mode_middle_action: + - switch.turn_on: gpio_switch2 + fan_mode_focus_action: + - switch.turn_on: gpio_switch1 + fan_mode_diffuse_action: + - switch.turn_on: gpio_switch2 + swing_off_action: + - switch.turn_on: gpio_switch1 + swing_horizontal_action: + - switch.turn_on: gpio_switch2 + swing_vertical_action: + - switch.turn_on: gpio_switch1 + swing_both_action: + - switch.turn_on: gpio_switch2 + hysteresis: 0.2 away_config: default_target_temperature_low: 16°C default_target_temperature_high: 20°C