#include "automation.h" #include "sprinkler.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include #include namespace esphome { namespace sprinkler { static const char *const TAG = "sprinkler"; SprinklerSwitch::SprinklerSwitch() {} SprinklerSwitch::SprinklerSwitch(switch_::Switch *sprinkler_switch) : on_switch_(sprinkler_switch) {} SprinklerSwitch::SprinklerSwitch(switch_::Switch *off_switch, switch_::Switch *on_switch, uint32_t pulse_duration) : pulse_duration_(pulse_duration), off_switch_(off_switch), on_switch_(on_switch) {} bool SprinklerSwitch::is_latching_valve() { return (this->off_switch_ != nullptr) && (this->on_switch_ != nullptr); } void SprinklerSwitch::loop() { if ((this->pinned_millis_) && (App.get_loop_component_start_time() > this->pinned_millis_ + this->pulse_duration_)) { this->pinned_millis_ = 0; // reset tracker if (this->off_switch_->state) { this->off_switch_->turn_off(); } if (this->on_switch_->state) { this->on_switch_->turn_off(); } } } void SprinklerSwitch::turn_off() { if (!this->state()) { // do nothing if we're already in the requested state return; } if (this->off_switch_ != nullptr) { // latching valve, start a pulse if (!this->off_switch_->state) { this->off_switch_->turn_on(); } this->pinned_millis_ = millis(); } else if (this->on_switch_ != nullptr) { // non-latching valve this->on_switch_->turn_off(); } this->state_ = false; } void SprinklerSwitch::turn_on() { if (this->state()) { // do nothing if we're already in the requested state return; } if (this->off_switch_ != nullptr) { // latching valve, start a pulse if (!this->on_switch_->state) { this->on_switch_->turn_on(); } this->pinned_millis_ = millis(); } else if (this->on_switch_ != nullptr) { // non-latching valve this->on_switch_->turn_on(); } this->state_ = true; } bool SprinklerSwitch::state() { if ((this->off_switch_ == nullptr) && (this->on_switch_ != nullptr)) { // latching valve is not configured... return this->on_switch_->state; // ...so just return the pump switch state } return this->state_; } void SprinklerSwitch::sync_valve_state(bool latch_state) { if (this->is_latching_valve()) { this->state_ = latch_state; } else if (this->on_switch_ != nullptr) { this->state_ = this->on_switch_->state; } } void SprinklerControllerNumber::setup() { float value; if (!this->restore_value_) { value = this->initial_value_; } else { this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); if (!this->pref_.load(&value)) { if (!std::isnan(this->initial_value_)) { value = this->initial_value_; } else { value = this->traits.get_min_value(); } } } this->publish_state(value); } void SprinklerControllerNumber::control(float value) { this->set_trigger_->trigger(value); this->publish_state(value); if (this->restore_value_) this->pref_.save(&value); } void SprinklerControllerNumber::dump_config() { LOG_NUMBER("", "Sprinkler Controller Number", this); } SprinklerControllerSwitch::SprinklerControllerSwitch() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {} void SprinklerControllerSwitch::loop() { if (!this->f_.has_value()) return; auto s = (*this->f_)(); if (!s.has_value()) return; this->publish_state(*s); } void SprinklerControllerSwitch::write_state(bool state) { if (this->prev_trigger_ != nullptr) { this->prev_trigger_->stop_action(); } if (state) { this->prev_trigger_ = this->turn_on_trigger_; this->turn_on_trigger_->trigger(); } else { this->prev_trigger_ = this->turn_off_trigger_; this->turn_off_trigger_->trigger(); } this->publish_state(state); } void SprinklerControllerSwitch::set_state_lambda(std::function()> &&f) { this->f_ = f; } float SprinklerControllerSwitch::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *SprinklerControllerSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } Trigger<> *SprinklerControllerSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } void SprinklerControllerSwitch::setup() { this->state = this->get_initial_state_with_restore_mode().value_or(false); } void SprinklerControllerSwitch::dump_config() { LOG_SWITCH("", "Sprinkler Switch", this); } SprinklerValveOperator::SprinklerValveOperator() {} SprinklerValveOperator::SprinklerValveOperator(SprinklerValve *valve, Sprinkler *controller) : controller_(controller), valve_(valve) {} void SprinklerValveOperator::loop() { uint32_t now = App.get_loop_component_start_time(); if (now >= this->start_millis_) { // dummy check switch (this->state_) { case STARTING: if (now > (this->start_millis_ + this->start_delay_)) { this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state } break; case ACTIVE: if (now > (this->start_millis_ + this->start_delay_ + this->run_duration_)) { this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down } break; case STOPPING: if (now > (this->stop_millis_ + this->stop_delay_)) { this->kill_(); // stop_delay_has been exceeded, ensure all valves are off } break; default: break; } } else { // perhaps millis() rolled over...or something else is horribly wrong! this->stop(); // bail out (TODO: handle this highly unlikely situation better...) } } void SprinklerValveOperator::set_controller(Sprinkler *controller) { if (controller != nullptr) { this->controller_ = controller; } } void SprinklerValveOperator::set_valve(SprinklerValve *valve) { if (valve != nullptr) { if (this->state_ != IDLE) { // Only kill if not already idle this->kill_(); // ensure everything is off before we let go! } this->state_ = IDLE; // reset state this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it this->start_millis_ = 0; // reset because (new) valve has not been started yet this->stop_millis_ = 0; // reset because (new) valve has not been started yet this->valve_ = valve; // finally, set the pointer to the new valve } } void SprinklerValveOperator::set_run_duration(uint32_t run_duration) { if (run_duration) { this->run_duration_ = run_duration * 1000; } } void SprinklerValveOperator::set_start_delay(uint32_t start_delay, bool start_delay_is_valve_delay) { this->start_delay_is_valve_delay_ = start_delay_is_valve_delay; this->start_delay_ = start_delay * 1000; // because 1000 milliseconds is one second } void SprinklerValveOperator::set_stop_delay(uint32_t stop_delay, bool stop_delay_is_valve_delay) { this->stop_delay_is_valve_delay_ = stop_delay_is_valve_delay; this->stop_delay_ = stop_delay * 1000; // because 1000 milliseconds is one second } void SprinklerValveOperator::start() { if (!this->run_duration_) { // can't start if zero run duration return; } if (this->start_delay_ && (this->pump_switch() != nullptr)) { this->state_ = STARTING; // STARTING state requires both a pump and a start_delay_ if (this->start_delay_is_valve_delay_) { this->pump_on_(); } else if (!this->pump_switch()->state()) { // if the pump is already on, wait to switch on the valve this->valve_on_(); // to ensure consistent run time } } else { this->run_(); // there is no start_delay_, so just start the pump and valve } this->stop_millis_ = 0; this->start_millis_ = millis(); // save the time the start request was made } void SprinklerValveOperator::stop() { if ((this->state_ == IDLE) || (this->state_ == STOPPING)) { // can't stop if already stopped or stopping return; } if (this->stop_delay_ && (this->pump_switch() != nullptr)) { this->state_ = STOPPING; // STOPPING state requires both a pump and a stop_delay_ if (this->stop_delay_is_valve_delay_) { this->pump_off_(); } else { this->valve_off_(); } if (this->pump_switch()->state()) { // if the pump is still on at this point, it may be in use... this->valve_off_(); // ...so just switch the valve off now to ensure consistent run time } } else { this->kill_(); // there is no stop_delay_, so just stop the pump and valve } this->stop_millis_ = millis(); // save the time the stop request was made } uint32_t SprinklerValveOperator::run_duration() { return this->run_duration_ / 1000; } uint32_t SprinklerValveOperator::time_remaining() { if (this->start_millis_ == 0) { return this->run_duration(); // hasn't been started yet } if (this->stop_millis_) { if (this->stop_millis_ - this->start_millis_ >= this->start_delay_ + this->run_duration_) { return 0; // valve was active for more than its configured duration, so we are done } else { // we're stopped; return time remaining return (this->run_duration_ - (this->stop_millis_ - this->start_millis_)) / 1000; } } auto completed_millis = this->start_millis_ + this->start_delay_ + this->run_duration_; if (completed_millis > millis()) { return (completed_millis - millis()) / 1000; // running now } return 0; // run completed } SprinklerState SprinklerValveOperator::state() { return this->state_; } SprinklerSwitch *SprinklerValveOperator::pump_switch() { if ((this->controller_ == nullptr) || (this->valve_ == nullptr)) { return nullptr; } if (this->valve_->pump_switch_index.has_value()) { return this->controller_->valve_pump_switch_by_pump_index(this->valve_->pump_switch_index.value()); } return nullptr; } void SprinklerValveOperator::pump_off_() { if ((this->valve_ == nullptr) || (this->pump_switch() == nullptr)) { // safety first! return; } if (this->controller_ == nullptr) { // safety first! this->pump_switch()->turn_off(); // if no controller was set, just switch off the pump } else { // ...otherwise, do it "safely" auto state = this->state_; // this is silly, but... this->state_ = BYPASS; // ...exclude me from the pump-in-use check that set_pump_state() does this->controller_->set_pump_state(this->pump_switch(), false); this->state_ = state; } } void SprinklerValveOperator::pump_on_() { if ((this->valve_ == nullptr) || (this->pump_switch() == nullptr)) { // safety first! return; } if (this->controller_ == nullptr) { // safety first! this->pump_switch()->turn_on(); // if no controller was set, just switch on the pump } else { // ...otherwise, do it "safely" auto state = this->state_; // this is silly, but... this->state_ = BYPASS; // ...exclude me from the pump-in-use check that set_pump_state() does this->controller_->set_pump_state(this->pump_switch(), true); this->state_ = state; } } void SprinklerValveOperator::valve_off_() { if (this->valve_ == nullptr) { // safety first! return; } if (this->valve_->valve_switch.state()) { this->valve_->valve_switch.turn_off(); } } void SprinklerValveOperator::valve_on_() { if (this->valve_ == nullptr) { // safety first! return; } if (!this->valve_->valve_switch.state()) { this->valve_->valve_switch.turn_on(); } } void SprinklerValveOperator::kill_() { this->state_ = IDLE; this->valve_off_(); this->pump_off_(); } void SprinklerValveOperator::run_() { this->state_ = ACTIVE; this->valve_on_(); this->pump_on_(); } SprinklerValveRunRequest::SprinklerValveRunRequest() {} SprinklerValveRunRequest::SprinklerValveRunRequest(size_t valve_number, uint32_t run_duration, SprinklerValveOperator *valve_op) : valve_number_(valve_number), run_duration_(run_duration), valve_op_(valve_op) {} bool SprinklerValveRunRequest::has_request() { return this->has_valve_; } bool SprinklerValveRunRequest::has_valve_operator() { return !(this->valve_op_ == nullptr); } void SprinklerValveRunRequest::set_request_from(SprinklerValveRunRequestOrigin origin) { this->origin_ = origin; } void SprinklerValveRunRequest::set_run_duration(uint32_t run_duration) { this->run_duration_ = run_duration; } void SprinklerValveRunRequest::set_valve(size_t valve_number) { this->valve_number_ = valve_number; this->run_duration_ = 0; this->valve_op_ = nullptr; this->has_valve_ = true; } void SprinklerValveRunRequest::set_valve_operator(SprinklerValveOperator *valve_op) { if (valve_op != nullptr) { this->valve_op_ = valve_op; } } void SprinklerValveRunRequest::reset() { this->has_valve_ = false; this->origin_ = USER; this->run_duration_ = 0; this->valve_op_ = nullptr; } uint32_t SprinklerValveRunRequest::run_duration() { return this->run_duration_; } size_t SprinklerValveRunRequest::valve() { return this->valve_number_; } optional SprinklerValveRunRequest::valve_as_opt() { if (this->has_valve_) { return this->valve_number_; } return nullopt; } SprinklerValveOperator *SprinklerValveRunRequest::valve_operator() { return this->valve_op_; } SprinklerValveRunRequestOrigin SprinklerValveRunRequest::request_is_from() { return this->origin_; } Sprinkler::Sprinkler() {} Sprinkler::Sprinkler(const std::string &name) { // The `name` is needed to set timers up, hence non-default constructor // replaces `set_name()` method previously existed this->name_ = name; this->timer_.push_back({this->name_ + "sm", false, 0, 0, std::bind(&Sprinkler::sm_timer_callback_, this)}); this->timer_.push_back({this->name_ + "vs", false, 0, 0, std::bind(&Sprinkler::valve_selection_callback_, this)}); } void Sprinkler::setup() { this->all_valves_off_(true); } void Sprinkler::loop() { for (auto &p : this->pump_) { p.loop(); } for (auto &v : this->valve_) { v.valve_switch.loop(); } for (auto &vo : this->valve_op_) { vo.loop(); } if (this->prev_req_.has_request() && this->prev_req_.valve_operator()->state() == IDLE) { this->prev_req_.reset(); } } void Sprinkler::add_valve(SprinklerControllerSwitch *valve_sw, SprinklerControllerSwitch *enable_sw) { auto new_valve_number = this->number_of_valves(); this->valve_.resize(new_valve_number + 1); SprinklerValve *new_valve = &this->valve_[new_valve_number]; new_valve->controller_switch = valve_sw; new_valve->controller_switch->set_state_lambda([this, new_valve_number]() -> optional { if (this->valve_pump_switch(new_valve_number) != nullptr) { return this->valve_switch(new_valve_number)->state() && this->valve_pump_switch(new_valve_number)->state(); } return this->valve_switch(new_valve_number)->state(); }); new_valve->valve_turn_off_automation = make_unique>(new_valve->controller_switch->get_turn_off_trigger()); new_valve->valve_shutdown_action = make_unique>(this); new_valve->valve_turn_off_automation->add_actions({new_valve->valve_shutdown_action.get()}); new_valve->valve_turn_on_automation = make_unique>(new_valve->controller_switch->get_turn_on_trigger()); new_valve->valve_resumeorstart_action = make_unique>(this); new_valve->valve_resumeorstart_action->set_valve_to_start(new_valve_number); new_valve->valve_turn_on_automation->add_actions({new_valve->valve_resumeorstart_action.get()}); if (enable_sw != nullptr) { new_valve->enable_switch = enable_sw; } } void Sprinkler::add_controller(Sprinkler *other_controller) { this->other_controllers_.push_back(other_controller); } void Sprinkler::set_controller_main_switch(SprinklerControllerSwitch *controller_switch) { this->controller_sw_ = controller_switch; controller_switch->set_state_lambda([this]() -> optional { for (size_t valve_number = 0; valve_number < this->number_of_valves(); valve_number++) { if (this->valve_[valve_number].controller_switch->state) { return true; } } return this->active_req_.has_request(); }); this->sprinkler_turn_off_automation_ = make_unique>(controller_switch->get_turn_off_trigger()); this->sprinkler_shutdown_action_ = make_unique>(this); this->sprinkler_turn_off_automation_->add_actions({sprinkler_shutdown_action_.get()}); this->sprinkler_turn_on_automation_ = make_unique>(controller_switch->get_turn_on_trigger()); this->sprinkler_resumeorstart_action_ = make_unique>(this); this->sprinkler_turn_on_automation_->add_actions({sprinkler_resumeorstart_action_.get()}); } void Sprinkler::set_controller_auto_adv_switch(SprinklerControllerSwitch *auto_adv_switch) { this->auto_adv_sw_ = auto_adv_switch; } void Sprinkler::set_controller_queue_enable_switch(SprinklerControllerSwitch *queue_enable_switch) { this->queue_enable_sw_ = queue_enable_switch; } void Sprinkler::set_controller_reverse_switch(SprinklerControllerSwitch *reverse_switch) { this->reverse_sw_ = reverse_switch; } void Sprinkler::set_controller_standby_switch(SprinklerControllerSwitch *standby_switch) { this->standby_sw_ = standby_switch; this->sprinkler_standby_turn_on_automation_ = make_unique>(standby_switch->get_turn_on_trigger()); this->sprinkler_standby_shutdown_action_ = make_unique>(this); this->sprinkler_standby_turn_on_automation_->add_actions({sprinkler_standby_shutdown_action_.get()}); } void Sprinkler::set_controller_multiplier_number(SprinklerControllerNumber *multiplier_number) { this->multiplier_number_ = multiplier_number; } void Sprinkler::set_controller_repeat_number(SprinklerControllerNumber *repeat_number) { this->repeat_number_ = repeat_number; } void Sprinkler::configure_valve_switch(size_t valve_number, switch_::Switch *valve_switch, uint32_t run_duration) { if (this->is_a_valid_valve(valve_number)) { this->valve_[valve_number].valve_switch.set_on_switch(valve_switch); this->valve_[valve_number].run_duration = run_duration; } } void Sprinkler::configure_valve_switch_pulsed(size_t valve_number, switch_::Switch *valve_switch_off, switch_::Switch *valve_switch_on, uint32_t pulse_duration, uint32_t run_duration) { if (this->is_a_valid_valve(valve_number)) { this->valve_[valve_number].valve_switch.set_off_switch(valve_switch_off); this->valve_[valve_number].valve_switch.set_on_switch(valve_switch_on); this->valve_[valve_number].valve_switch.set_pulse_duration(pulse_duration); this->valve_[valve_number].run_duration = run_duration; } } void Sprinkler::configure_valve_pump_switch(size_t valve_number, switch_::Switch *pump_switch) { if (this->is_a_valid_valve(valve_number)) { for (size_t i = 0; i < this->pump_.size(); i++) { // check each existing registered pump if (this->pump_[i].on_switch() == pump_switch) { // if the "new" pump matches one we already have... this->valve_[valve_number].pump_switch_index = i; // ...save its index in the SprinklerSwitch vector pump_... return; // ...and we are done } } // if we end up here, no pumps matched, so add a new one and set the valve's SprinklerSwitch at it this->pump_.resize(this->pump_.size() + 1); this->pump_.back().set_on_switch(pump_switch); this->valve_[valve_number].pump_switch_index = this->pump_.size() - 1; // save the index to the new pump } } void Sprinkler::configure_valve_pump_switch_pulsed(size_t valve_number, switch_::Switch *pump_switch_off, switch_::Switch *pump_switch_on, uint32_t pulse_duration) { if (this->is_a_valid_valve(valve_number)) { for (size_t i = 0; i < this->pump_.size(); i++) { // check each existing registered pump if ((this->pump_[i].off_switch() == pump_switch_off) && (this->pump_[i].on_switch() == pump_switch_on)) { // if the "new" pump matches one we already have... this->valve_[valve_number].pump_switch_index = i; // ...save its index in the SprinklerSwitch vector pump_... return; // ...and we are done } } // if we end up here, no pumps matched, so add a new one and set the valve's SprinklerSwitch at it this->pump_.resize(this->pump_.size() + 1); this->pump_.back().set_off_switch(pump_switch_off); this->pump_.back().set_on_switch(pump_switch_on); this->pump_.back().set_pulse_duration(pulse_duration); this->valve_[valve_number].pump_switch_index = this->pump_.size() - 1; // save the index to the new pump } } void Sprinkler::configure_valve_run_duration_number(size_t valve_number, SprinklerControllerNumber *run_duration_number) { if (this->is_a_valid_valve(valve_number)) { this->valve_[valve_number].run_duration_number = run_duration_number; } } void Sprinkler::set_divider(optional divider) { if (!divider.has_value()) { return; } if (divider.value() > 0) { this->set_multiplier(1.0 / divider.value()); this->set_repeat(divider.value() - 1); } else if (divider.value() == 0) { this->set_multiplier(1.0); this->set_repeat(0); } } void Sprinkler::set_multiplier(const optional multiplier) { if ((!multiplier.has_value()) || (multiplier.value() < 0)) { return; } this->multiplier_ = multiplier.value(); if (this->multiplier_number_ == nullptr) { return; } if (this->multiplier_number_->state == multiplier.value()) { return; } auto call = this->multiplier_number_->make_call(); call.set_value(multiplier.value()); call.perform(); } void Sprinkler::set_next_prev_ignore_disabled_valves(bool ignore_disabled) { this->next_prev_ignore_disabled_ = ignore_disabled; } void Sprinkler::set_pump_start_delay(uint32_t start_delay) { this->start_delay_is_valve_delay_ = false; this->start_delay_ = start_delay; } void Sprinkler::set_pump_stop_delay(uint32_t stop_delay) { this->stop_delay_is_valve_delay_ = false; this->stop_delay_ = stop_delay; } void Sprinkler::set_valve_start_delay(uint32_t start_delay) { this->start_delay_is_valve_delay_ = true; this->start_delay_ = start_delay; } void Sprinkler::set_valve_stop_delay(uint32_t stop_delay) { this->stop_delay_is_valve_delay_ = true; this->stop_delay_ = stop_delay; } void Sprinkler::set_pump_switch_off_during_valve_open_delay(bool pump_switch_off_during_valve_open_delay) { this->pump_switch_off_during_valve_open_delay_ = pump_switch_off_during_valve_open_delay; } void Sprinkler::set_valve_open_delay(const uint32_t valve_open_delay) { if (valve_open_delay > 0) { this->valve_overlap_ = false; this->switching_delay_ = valve_open_delay; } else { this->switching_delay_.reset(); } } void Sprinkler::set_valve_overlap(uint32_t valve_overlap) { if (valve_overlap > 0) { this->valve_overlap_ = true; this->switching_delay_ = valve_overlap; } else { this->switching_delay_.reset(); } this->pump_switch_off_during_valve_open_delay_ = false; // incompatible option } void Sprinkler::set_manual_selection_delay(uint32_t manual_selection_delay) { if (manual_selection_delay > 0) { this->manual_selection_delay_ = manual_selection_delay; } else { this->manual_selection_delay_.reset(); } } void Sprinkler::set_valve_run_duration(const optional valve_number, const optional run_duration) { if (!valve_number.has_value() || !run_duration.has_value()) { return; } if (!this->is_a_valid_valve(valve_number.value())) { return; } this->valve_[valve_number.value()].run_duration = run_duration.value(); if (this->valve_[valve_number.value()].run_duration_number == nullptr) { return; } if (this->valve_[valve_number.value()].run_duration_number->state == run_duration.value()) { return; } auto call = this->valve_[valve_number.value()].run_duration_number->make_call(); if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement() == MIN_STR) { call.set_value(run_duration.value() / 60.0); } else { call.set_value(run_duration.value()); } call.perform(); } void Sprinkler::set_auto_advance(const bool auto_advance) { if (this->auto_adv_sw_ == nullptr) { return; } if (this->auto_adv_sw_->state == auto_advance) { return; } if (auto_advance) { this->auto_adv_sw_->turn_on(); } else { this->auto_adv_sw_->turn_off(); } } void Sprinkler::set_repeat(optional repeat) { this->target_repeats_ = repeat; if (this->repeat_number_ == nullptr) { return; } if (this->repeat_number_->state == repeat.value()) { return; } auto call = this->repeat_number_->make_call(); call.set_value(repeat.value_or(0)); call.perform(); } void Sprinkler::set_queue_enable(bool queue_enable) { if (this->queue_enable_sw_ == nullptr) { return; } if (this->queue_enable_sw_->state == queue_enable) { return; } if (queue_enable) { this->queue_enable_sw_->turn_on(); } else { this->queue_enable_sw_->turn_off(); } } void Sprinkler::set_reverse(const bool reverse) { if (this->reverse_sw_ == nullptr) { return; } if (this->reverse_sw_->state == reverse) { return; } if (reverse) { this->reverse_sw_->turn_on(); } else { this->reverse_sw_->turn_off(); } } void Sprinkler::set_standby(const bool standby) { if (this->standby_sw_ == nullptr) { return; } if (this->standby_sw_->state == standby) { return; } if (standby) { this->standby_sw_->turn_on(); } else { this->standby_sw_->turn_off(); } } uint32_t Sprinkler::valve_run_duration(const size_t valve_number) { if (!this->is_a_valid_valve(valve_number)) { return 0; } if (this->valve_[valve_number].run_duration_number != nullptr) { if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement() == MIN_STR) { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state * 60)); } else { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state)); } } return this->valve_[valve_number].run_duration; } uint32_t Sprinkler::valve_run_duration_adjusted(const size_t valve_number) { uint32_t run_duration = 0; if (this->is_a_valid_valve(valve_number)) { run_duration = this->valve_run_duration(valve_number); } run_duration = static_cast(roundf(run_duration * this->multiplier())); // run_duration must not be less than any of these if ((run_duration < this->start_delay_) || (run_duration < this->stop_delay_) || (run_duration < this->switching_delay_.value_or(0) * 2)) { return std::max(this->switching_delay_.value_or(0) * 2, std::max(this->start_delay_, this->stop_delay_)); } return run_duration; } bool Sprinkler::auto_advance() { if (this->auto_adv_sw_ != nullptr) { return this->auto_adv_sw_->state; } return true; } float Sprinkler::multiplier() { if (this->multiplier_number_ != nullptr) { return this->multiplier_number_->state; } return this->multiplier_; } optional Sprinkler::repeat() { if (this->repeat_number_ != nullptr) { return static_cast(roundf(this->repeat_number_->state)); } return this->target_repeats_; } optional Sprinkler::repeat_count() { // if there is an active valve and auto-advance is enabled, we may be repeating, so return the count if (this->active_req_.has_request() && this->auto_advance()) { return this->repeat_count_; } return nullopt; } bool Sprinkler::queue_enabled() { if (this->queue_enable_sw_ != nullptr) { return this->queue_enable_sw_->state; } return true; } bool Sprinkler::reverse() { if (this->reverse_sw_ != nullptr) { return this->reverse_sw_->state; } return false; } bool Sprinkler::standby() { if (this->standby_sw_ != nullptr) { return this->standby_sw_->state; } return false; } void Sprinkler::start_from_queue() { if (this->standby()) { ESP_LOGD(TAG, "start_from_queue called but standby is enabled; no action taken"); return; } if (this->multiplier() == 0) { ESP_LOGD(TAG, "start_from_queue called but multiplier is set to zero; no action taken"); return; } if (this->queued_valves_.empty()) { return; // if there is nothing in the queue, don't do anything } if (this->queue_enabled() && this->active_valve().has_value()) { return; // if there is already a valve running from the queue, do nothing } this->set_auto_advance(false); this->set_queue_enable(true); this->reset_cycle_states_(); // just in case auto-advance is switched on later this->repeat_count_ = 0; this->fsm_kick_(); // will automagically pick up from the queue (it has priority) } void Sprinkler::start_full_cycle() { if (this->standby()) { ESP_LOGD(TAG, "start_full_cycle called but standby is enabled; no action taken"); return; } if (this->multiplier() == 0) { ESP_LOGD(TAG, "start_full_cycle called but multiplier is set to zero; no action taken"); return; } if (this->auto_advance() && this->active_valve().has_value()) { return; // if auto-advance is already enabled and there is already a valve running, do nothing } this->set_queue_enable(false); this->prep_full_cycle_(); this->repeat_count_ = 0; // if there is no active valve already, start the first valve in the cycle if (!this->active_req_.has_request()) { this->fsm_kick_(); } } void Sprinkler::start_single_valve(const optional valve_number, optional run_duration) { if (this->standby()) { ESP_LOGD(TAG, "start_single_valve called but standby is enabled; no action taken"); return; } if (this->multiplier() == 0) { ESP_LOGD(TAG, "start_single_valve called but multiplier is set to zero; no action taken"); return; } if (!valve_number.has_value() || (valve_number == this->active_valve())) { return; } this->set_auto_advance(false); this->set_queue_enable(false); this->reset_cycle_states_(); // just in case auto-advance is switched on later this->repeat_count_ = 0; this->fsm_request_(valve_number.value(), run_duration.value_or(0)); } void Sprinkler::queue_valve(optional valve_number, optional run_duration) { if (valve_number.has_value()) { if (this->is_a_valid_valve(valve_number.value()) && (this->queued_valves_.size() < this->max_queue_size_)) { SprinklerQueueItem item{valve_number.value(), run_duration.value()}; this->queued_valves_.insert(this->queued_valves_.begin(), item); ESP_LOGD(TAG, "Valve %zu placed into queue with run duration of %" PRIu32 " seconds", valve_number.value_or(0), run_duration.value_or(0)); } } } void Sprinkler::clear_queued_valves() { this->queued_valves_.clear(); ESP_LOGD(TAG, "Queue cleared"); } void Sprinkler::next_valve() { if (this->state_ == IDLE) { this->reset_cycle_states_(); // just in case auto-advance is switched on later } this->manual_valve_ = this->next_valve_number_( this->manual_valve_.value_or(this->active_req_.valve_as_opt().value_or(this->number_of_valves() - 1)), !this->next_prev_ignore_disabled_, true); if (!this->manual_valve_.has_value()) { ESP_LOGD(TAG, "next_valve was called but no valve could be started; perhaps next_prev_ignore_disabled allows only " "enabled valves and no valves are enabled?"); return; } if (this->manual_selection_delay_.has_value()) { this->set_timer_duration_(sprinkler::TIMER_VALVE_SELECTION, this->manual_selection_delay_.value()); this->start_timer_(sprinkler::TIMER_VALVE_SELECTION); } else { this->fsm_request_(this->manual_valve_.value()); } } void Sprinkler::previous_valve() { if (this->state_ == IDLE) { this->reset_cycle_states_(); // just in case auto-advance is switched on later } this->manual_valve_ = this->previous_valve_number_(this->manual_valve_.value_or(this->active_req_.valve_as_opt().value_or(0)), !this->next_prev_ignore_disabled_, true); if (!this->manual_valve_.has_value()) { ESP_LOGD(TAG, "previous_valve was called but no valve could be started; perhaps next_prev_ignore_disabled allows " "only enabled valves and no valves are enabled?"); return; } if (this->manual_selection_delay_.has_value()) { this->set_timer_duration_(sprinkler::TIMER_VALVE_SELECTION, this->manual_selection_delay_.value()); this->start_timer_(sprinkler::TIMER_VALVE_SELECTION); } else { this->fsm_request_(this->manual_valve_.value()); } } void Sprinkler::shutdown(bool clear_queue) { this->cancel_timer_(sprinkler::TIMER_VALVE_SELECTION); this->active_req_.reset(); this->manual_valve_.reset(); this->next_req_.reset(); for (auto &vo : this->valve_op_) { vo.stop(); } this->fsm_transition_to_shutdown_(); if (clear_queue) { this->clear_queued_valves(); this->repeat_count_ = 0; } } void Sprinkler::pause() { if (this->paused_valve_.has_value() || !this->active_req_.has_request()) { return; // we can't pause if we're already paused or if there is no active valve } this->paused_valve_ = this->active_valve(); this->resume_duration_ = this->time_remaining_active_valve(); this->shutdown(false); ESP_LOGD(TAG, "Paused valve %zu with %" PRIu32 " seconds remaining", this->paused_valve_.value_or(0), this->resume_duration_.value_or(0)); } void Sprinkler::resume() { if (this->standby()) { ESP_LOGD(TAG, "resume called but standby is enabled; no action taken"); return; } if (this->paused_valve_.has_value() && (this->resume_duration_.has_value())) { // Resume only if valve has not been completed yet if (!this->valve_cycle_complete_(this->paused_valve_.value())) { ESP_LOGD(TAG, "Resuming valve %zu with %" PRIu32 " seconds remaining", this->paused_valve_.value_or(0), this->resume_duration_.value_or(0)); this->fsm_request_(this->paused_valve_.value(), this->resume_duration_.value()); } this->reset_resume(); } else { ESP_LOGD(TAG, "No valve to resume!"); } } void Sprinkler::resume_or_start_full_cycle() { if (this->paused_valve_.has_value() && (this->resume_duration_.has_value())) { this->resume(); } else { this->start_full_cycle(); } } void Sprinkler::reset_resume() { this->paused_valve_.reset(); this->resume_duration_.reset(); } const char *Sprinkler::valve_name(const size_t valve_number) { if (this->is_a_valid_valve(valve_number)) { return this->valve_[valve_number].controller_switch->get_name().c_str(); } return nullptr; } optional Sprinkler::active_valve_request_is_from() { if (this->active_req_.has_request()) { return this->active_req_.request_is_from(); } return nullopt; } optional Sprinkler::active_valve() { if (!this->valve_overlap_ && this->prev_req_.has_request() && (this->prev_req_.valve_operator()->state() == STARTING || this->prev_req_.valve_operator()->state() == ACTIVE)) { return this->prev_req_.valve_as_opt(); } return this->active_req_.valve_as_opt(); } optional Sprinkler::paused_valve() { return this->paused_valve_; } optional Sprinkler::queued_valve() { if (!this->queued_valves_.empty()) { return this->queued_valves_.back().valve_number; } return nullopt; } optional Sprinkler::manual_valve() { return this->manual_valve_; } size_t Sprinkler::number_of_valves() { return this->valve_.size(); } bool Sprinkler::is_a_valid_valve(const size_t valve_number) { return ((valve_number >= 0) && (valve_number < this->number_of_valves())); } bool Sprinkler::pump_in_use(SprinklerSwitch *pump_switch) { if (pump_switch == nullptr) { return false; // we can't do anything if there's nothing to check } // a pump must be considered "in use" if a (distribution) valve it supplies is active. this means: // - at least one SprinklerValveOperator: // - has a valve loaded that depends on this pump // - is in a state that depends on the pump: (ACTIVE and _possibly_ STARTING/STOPPING) // - if NO SprinklerValveOperator is active but there is a run request pending (active_req_.has_request()) and the // controller state is STARTING, valve open delay is configured but NOT pump_switch_off_during_valve_open_delay_ for (auto &vo : this->valve_op_) { // first, check if any SprinklerValveOperator has a valve dependent on this pump if ((vo.state() != BYPASS) && (vo.pump_switch() != nullptr)) { // the SprinklerValveOperator is configured with a pump; now check if it is the pump of interest if ((vo.pump_switch()->off_switch() == pump_switch->off_switch()) && (vo.pump_switch()->on_switch() == pump_switch->on_switch())) { // now if the SprinklerValveOperator has a pump and it is either ACTIVE, is STARTING with a valve delay or // is STOPPING with a valve delay, its pump can be considered "in use", so just return indicating this now if ((vo.state() == ACTIVE) || ((vo.state() == STARTING) && this->start_delay_ && this->start_delay_is_valve_delay_) || ((vo.state() == STOPPING) && this->stop_delay_ && this->stop_delay_is_valve_delay_)) { return true; } } } } // if we end up here, no SprinklerValveOperator was in a "give-away" state indicating that the pump is in use... if (!this->valve_overlap_ && !this->pump_switch_off_during_valve_open_delay_ && this->switching_delay_.has_value() && this->active_req_.has_request() && (this->state_ != STOPPING)) { // ...the controller is configured to keep the pump on during a valve open delay, so just return // whether or not the next valve shares the same pump return (pump_switch->off_switch() == this->valve_pump_switch(this->active_req_.valve())->off_switch()) && (pump_switch->on_switch() == this->valve_pump_switch(this->active_req_.valve())->on_switch()); } return false; } void Sprinkler::set_pump_state(SprinklerSwitch *pump_switch, bool state) { if (pump_switch == nullptr) { return; // we can't do anything if there's nothing to check } bool hold_pump_on = false; for (auto &controller : this->other_controllers_) { // check if the pump is in use by another controller if (controller != this) { // dummy check if (controller->pump_in_use(pump_switch)) { hold_pump_on = true; // if another controller says it's using this pump, keep it on // at this point we know if there exists another SprinklerSwitch that is "on" with its // off_switch_ and on_switch_ pointers pointing to the same pair of switch objects } } } if (hold_pump_on) { // at this point we know if there exists another SprinklerSwitch that is "on" with its // off_switch_ and on_switch_ pointers pointing to the same pair of switch objects... pump_switch->sync_valve_state(true); // ...so ensure our state is consistent ESP_LOGD(TAG, "Leaving pump on because another controller instance is using it"); } if (state) { // ...and now we can set the new state of the switch pump_switch->turn_on(); } else if (!hold_pump_on && !this->pump_in_use(pump_switch)) { pump_switch->turn_off(); } else if (hold_pump_on) { // we must assume the other controller will switch off the pump when done... pump_switch->sync_valve_state(false); // ...this only impacts latching valves } } uint32_t Sprinkler::total_cycle_time_all_valves() { uint32_t total_time_remaining = 0; for (size_t valve = 0; valve < this->number_of_valves(); valve++) { total_time_remaining += this->valve_run_duration_adjusted(valve); } if (this->valve_overlap_) { total_time_remaining -= this->switching_delay_.value_or(0) * (this->number_of_valves() - 1); } else { total_time_remaining += this->switching_delay_.value_or(0) * (this->number_of_valves() - 1); } return total_time_remaining; } uint32_t Sprinkler::total_cycle_time_enabled_valves() { uint32_t total_time_remaining = 0; uint32_t valve_count = 0; for (size_t valve = 0; valve < this->number_of_valves(); valve++) { if (this->valve_is_enabled_(valve)) { total_time_remaining += this->valve_run_duration_adjusted(valve); valve_count++; } } if (valve_count) { if (this->valve_overlap_) { total_time_remaining -= this->switching_delay_.value_or(0) * (valve_count - 1); } else { total_time_remaining += this->switching_delay_.value_or(0) * (valve_count - 1); } } return total_time_remaining; } uint32_t Sprinkler::total_cycle_time_enabled_incomplete_valves() { uint32_t total_time_remaining = 0; uint32_t enabled_valve_count = 0; uint32_t incomplete_valve_count = 0; for (size_t valve = 0; valve < this->number_of_valves(); valve++) { if (this->valve_is_enabled_(valve)) { enabled_valve_count++; if (!this->valve_cycle_complete_(valve)) { if (!this->active_valve().has_value() || (valve != this->active_valve().value())) { total_time_remaining += this->valve_run_duration_adjusted(valve); incomplete_valve_count++; } else { // to get here, there must be an active valve and this valve must be equal to 'valve' if (this->active_req_.valve_operator() == nullptr) { // no SVO has been assigned yet so it hasn't started total_time_remaining += this->valve_run_duration_adjusted(valve); incomplete_valve_count++; } } } } } if (incomplete_valve_count >= enabled_valve_count) { incomplete_valve_count--; } if (incomplete_valve_count) { if (this->valve_overlap_) { total_time_remaining -= this->switching_delay_.value_or(0) * incomplete_valve_count; } else { total_time_remaining += this->switching_delay_.value_or(0) * incomplete_valve_count; } } return total_time_remaining; } uint32_t Sprinkler::total_queue_time() { uint32_t total_time_remaining = 0; uint32_t valve_count = 0; for (auto &valve : this->queued_valves_) { if (valve.run_duration) { total_time_remaining += valve.run_duration; } else { total_time_remaining += this->valve_run_duration_adjusted(valve.valve_number); } valve_count++; } if (valve_count) { if (this->valve_overlap_) { total_time_remaining -= this->switching_delay_.value_or(0) * (valve_count - 1); } else { total_time_remaining += this->switching_delay_.value_or(0) * (valve_count - 1); } } return total_time_remaining; } optional Sprinkler::time_remaining_active_valve() { if (this->active_req_.has_request()) { // first try to return the value based on active_req_... if (this->active_req_.valve_operator() != nullptr) { return this->active_req_.valve_operator()->time_remaining(); } } if (this->prev_req_.has_request()) { // try to return the value based on prev_req_... if (this->prev_req_.valve_operator() != nullptr) { return this->prev_req_.valve_operator()->time_remaining(); } } return nullopt; } optional Sprinkler::time_remaining_current_operation() { if (!this->time_remaining_active_valve().has_value() && this->state_ == IDLE) { return nullopt; } auto total_time_remaining = this->time_remaining_active_valve().value_or(0); if (this->auto_advance()) { total_time_remaining += this->total_cycle_time_enabled_incomplete_valves(); if (this->repeat().value_or(0) > 0) { total_time_remaining += (this->total_cycle_time_enabled_valves() * (this->repeat().value_or(0) - this->repeat_count().value_or(0))); } } if (this->queue_enabled()) { total_time_remaining += this->total_queue_time(); } return total_time_remaining; } bool Sprinkler::any_controller_is_active() { if (this->state_ != IDLE) { return true; } for (auto &controller : this->other_controllers_) { if (controller != this) { // dummy check if (controller->controller_state() != IDLE) { return true; } } } return false; } SprinklerControllerSwitch *Sprinkler::control_switch(size_t valve_number) { if (this->is_a_valid_valve(valve_number)) { return this->valve_[valve_number].controller_switch; } return nullptr; } SprinklerControllerSwitch *Sprinkler::enable_switch(size_t valve_number) { if (this->is_a_valid_valve(valve_number)) { return this->valve_[valve_number].enable_switch; } return nullptr; } SprinklerSwitch *Sprinkler::valve_switch(const size_t valve_number) { if (this->is_a_valid_valve(valve_number)) { return &this->valve_[valve_number].valve_switch; } return nullptr; } SprinklerSwitch *Sprinkler::valve_pump_switch(const size_t valve_number) { if (this->is_a_valid_valve(valve_number) && this->valve_[valve_number].pump_switch_index.has_value()) { return &this->pump_[this->valve_[valve_number].pump_switch_index.value()]; } return nullptr; } SprinklerSwitch *Sprinkler::valve_pump_switch_by_pump_index(size_t pump_index) { if (pump_index < this->pump_.size()) { return &this->pump_[pump_index]; } return nullptr; } bool Sprinkler::valve_is_enabled_(const size_t valve_number) { if (this->is_a_valid_valve(valve_number)) { if (this->valve_[valve_number].enable_switch != nullptr) { return this->valve_[valve_number].enable_switch->state; } else { return true; } } return false; } void Sprinkler::mark_valve_cycle_complete_(const size_t valve_number) { if (this->is_a_valid_valve(valve_number)) { ESP_LOGD(TAG, "Marking valve %u complete", valve_number); this->valve_[valve_number].valve_cycle_complete = true; } } bool Sprinkler::valve_cycle_complete_(const size_t valve_number) { if (this->is_a_valid_valve(valve_number)) { return this->valve_[valve_number].valve_cycle_complete; } return false; } optional Sprinkler::next_valve_number_(const optional first_valve, const bool include_disabled, const bool include_complete) { auto valve = first_valve.value_or(0); size_t start = first_valve.has_value() ? 1 : 0; if (!this->is_a_valid_valve(valve)) { valve = 0; } for (size_t offset = start; offset < this->number_of_valves(); offset++) { auto valve_of_interest = valve + offset; if (!this->is_a_valid_valve(valve_of_interest)) { valve_of_interest -= this->number_of_valves(); } if ((this->valve_is_enabled_(valve_of_interest) || include_disabled) && (!this->valve_cycle_complete_(valve_of_interest) || include_complete)) { return valve_of_interest; } } return nullopt; } optional Sprinkler::previous_valve_number_(const optional first_valve, const bool include_disabled, const bool include_complete) { auto valve = first_valve.value_or(this->number_of_valves() - 1); size_t start = first_valve.has_value() ? 1 : 0; if (!this->is_a_valid_valve(valve)) { valve = this->number_of_valves() - 1; } for (size_t offset = start; offset < this->number_of_valves(); offset++) { auto valve_of_interest = valve - offset; if (!this->is_a_valid_valve(valve_of_interest)) { valve_of_interest += this->number_of_valves(); } if ((this->valve_is_enabled_(valve_of_interest) || include_disabled) && (!this->valve_cycle_complete_(valve_of_interest) || include_complete)) { return valve_of_interest; } } return nullopt; } optional Sprinkler::next_valve_number_in_cycle_(const optional first_valve) { if (this->reverse()) { return this->previous_valve_number_(first_valve, false, false); } return this->next_valve_number_(first_valve, false, false); } void Sprinkler::load_next_valve_run_request_(const optional first_valve) { if (this->active_req_.has_request()) { this->prev_req_ = this->active_req_; } else { this->prev_req_.reset(); } if (this->next_req_.has_request()) { if (!this->next_req_.run_duration()) { // ensure the run duration is set correctly for consumption later on this->next_req_.set_run_duration(this->valve_run_duration_adjusted(this->next_req_.valve())); } return; // there is already a request pending } else if (this->queue_enabled() && !this->queued_valves_.empty()) { this->next_req_.set_valve(this->queued_valves_.back().valve_number); this->next_req_.set_request_from(QUEUE); if (this->queued_valves_.back().run_duration) { this->next_req_.set_run_duration(this->queued_valves_.back().run_duration); this->queued_valves_.pop_back(); } else if (this->multiplier()) { this->next_req_.set_run_duration(this->valve_run_duration_adjusted(this->queued_valves_.back().valve_number)); this->queued_valves_.pop_back(); } else { this->next_req_.reset(); } } else if (this->auto_advance() && this->multiplier()) { if (this->next_valve_number_in_cycle_(first_valve).has_value()) { // if there is another valve to run as a part of a cycle, load that this->next_req_.set_valve(this->next_valve_number_in_cycle_(first_valve).value_or(0)); this->next_req_.set_request_from(CYCLE); this->next_req_.set_run_duration( this->valve_run_duration_adjusted(this->next_valve_number_in_cycle_(first_valve).value_or(0))); } else if ((this->repeat_count_++ < this->repeat().value_or(0))) { ESP_LOGD(TAG, "Repeating - starting cycle %" PRIu32 " of %" PRIu32, this->repeat_count_ + 1, this->repeat().value_or(0) + 1); // if there are repeats remaining and no more valves were left in the cycle, start a new cycle this->prep_full_cycle_(); if (this->next_valve_number_in_cycle_().has_value()) { // this should always succeed here, but just in case... this->next_req_.set_valve(this->next_valve_number_in_cycle_().value_or(0)); this->next_req_.set_request_from(CYCLE); this->next_req_.set_run_duration( this->valve_run_duration_adjusted(this->next_valve_number_in_cycle_().value_or(0))); } } } } bool Sprinkler::any_valve_is_enabled_() { for (size_t valve_number = 0; valve_number < this->number_of_valves(); valve_number++) { if (this->valve_is_enabled_(valve_number)) return true; } return false; } void Sprinkler::start_valve_(SprinklerValveRunRequest *req) { if (!req->has_request()) { return; // we can't do anything if the request contains nothing } if (!this->is_a_valid_valve(req->valve())) { return; // we can't do anything if the valve number isn't valid } for (auto &vo : this->valve_op_) { // find the first available SprinklerValveOperator, load it and start it up if (vo.state() == IDLE) { auto run_duration = req->run_duration() ? req->run_duration() : this->valve_run_duration_adjusted(req->valve()); ESP_LOGD(TAG, "%s is starting valve %zu for %" PRIu32 " seconds, cycle %" PRIu32 " of %" PRIu32, this->req_as_str_(req->request_is_from()).c_str(), req->valve(), run_duration, this->repeat_count_ + 1, this->repeat().value_or(0) + 1); req->set_valve_operator(&vo); vo.set_controller(this); vo.set_valve(&this->valve_[req->valve()]); vo.set_run_duration(run_duration); vo.set_start_delay(this->start_delay_, this->start_delay_is_valve_delay_); vo.set_stop_delay(this->stop_delay_, this->stop_delay_is_valve_delay_); vo.start(); return; } } } void Sprinkler::all_valves_off_(const bool include_pump) { for (size_t valve_index = 0; valve_index < this->number_of_valves(); valve_index++) { if (this->valve_[valve_index].valve_switch.state()) { this->valve_[valve_index].valve_switch.turn_off(); } if (include_pump) { this->set_pump_state(this->valve_pump_switch(valve_index), false); } } ESP_LOGD(TAG, "All valves stopped%s", include_pump ? ", including pumps" : ""); } void Sprinkler::prep_full_cycle_() { this->set_auto_advance(true); if (!this->any_valve_is_enabled_()) { for (auto &valve : this->valve_) { if (valve.enable_switch != nullptr) { if (!valve.enable_switch->state) { valve.enable_switch->turn_on(); } } } } this->reset_cycle_states_(); } void Sprinkler::reset_cycle_states_() { for (auto &valve : this->valve_) { valve.valve_cycle_complete = false; } } void Sprinkler::fsm_request_(size_t requested_valve, uint32_t requested_run_duration) { this->next_req_.set_valve(requested_valve); this->next_req_.set_run_duration(requested_run_duration); // if state is IDLE or ACTIVE, call fsm_transition_() to start it immediately; // otherwise, fsm_transition() will pick up next_req_ at the next appropriate transition this->fsm_kick_(); } void Sprinkler::fsm_kick_() { if ((this->state_ == IDLE) || (this->state_ == ACTIVE)) { this->fsm_transition_(); } } void Sprinkler::fsm_transition_() { ESP_LOGVV(TAG, "fsm_transition_ called; state is %s", this->state_as_str_(this->state_).c_str()); switch (this->state_) { case IDLE: // the system was off -> start it up // advances to ACTIVE this->fsm_transition_from_shutdown_(); break; case ACTIVE: // advances to STOPPING or ACTIVE (again) this->fsm_transition_from_valve_run_(); break; case STARTING: { // follows valve open delay interval this->set_timer_duration_(sprinkler::TIMER_SM, this->active_req_.run_duration() - this->switching_delay_.value_or(0)); this->start_timer_(sprinkler::TIMER_SM); this->start_valve_(&this->active_req_); this->state_ = ACTIVE; if (this->next_req_.has_request()) { // another valve has been requested, so restart the timer so we pick it up quickly this->set_timer_duration_(sprinkler::TIMER_SM, this->manual_selection_delay_.value_or(1)); this->start_timer_(sprinkler::TIMER_SM); } break; } case STOPPING: // stop_delay_ has elapsed so just shut everything off this->active_req_.reset(); this->manual_valve_.reset(); this->all_valves_off_(true); this->state_ = IDLE; break; default: break; } if (this->next_req_.has_request() && (this->state_ == IDLE)) { // another valve has been requested, so restart the timer so we pick it up quickly this->set_timer_duration_(sprinkler::TIMER_SM, this->manual_selection_delay_.value_or(1)); this->start_timer_(sprinkler::TIMER_SM); } ESP_LOGVV(TAG, "fsm_transition_ complete; new state is %s", this->state_as_str_(this->state_).c_str()); } void Sprinkler::fsm_transition_from_shutdown_() { this->load_next_valve_run_request_(); if (this->next_req_.has_request()) { // there is a valve to run... this->active_req_.set_valve(this->next_req_.valve()); this->active_req_.set_request_from(this->next_req_.request_is_from()); this->active_req_.set_run_duration(this->next_req_.run_duration()); this->next_req_.reset(); this->set_timer_duration_(sprinkler::TIMER_SM, this->active_req_.run_duration() - this->switching_delay_.value_or(0)); this->start_timer_(sprinkler::TIMER_SM); this->start_valve_(&this->active_req_); this->state_ = ACTIVE; } } void Sprinkler::fsm_transition_from_valve_run_() { if (!this->active_req_.has_request()) { // dummy check... this->fsm_transition_to_shutdown_(); return; } if (!this->timer_active_(sprinkler::TIMER_SM)) { // only flag the valve as "complete" if the timer finished if ((this->active_req_.request_is_from() == CYCLE) || (this->active_req_.request_is_from() == USER)) { this->mark_valve_cycle_complete_(this->active_req_.valve()); } } else { ESP_LOGD(TAG, "Valve cycle interrupted - NOT flagging valve as complete and stopping current valve"); for (auto &vo : this->valve_op_) { vo.stop(); } } this->load_next_valve_run_request_(this->active_req_.valve()); if (this->next_req_.has_request()) { // there is another valve to run... bool same_pump = this->valve_pump_switch(this->active_req_.valve()) == this->valve_pump_switch(this->next_req_.valve()); this->active_req_.set_valve(this->next_req_.valve()); this->active_req_.set_request_from(this->next_req_.request_is_from()); this->active_req_.set_run_duration(this->next_req_.run_duration()); this->next_req_.reset(); // this->state_ = ACTIVE; // state isn't changing if (this->valve_overlap_ || !this->switching_delay_.has_value()) { this->set_timer_duration_(sprinkler::TIMER_SM, this->active_req_.run_duration() - this->switching_delay_.value_or(0)); this->start_timer_(sprinkler::TIMER_SM); this->start_valve_(&this->active_req_); } else { this->set_timer_duration_( sprinkler::TIMER_SM, this->switching_delay_.value() * 2 + (this->pump_switch_off_during_valve_open_delay_ && same_pump ? this->stop_delay_ : 0)); this->start_timer_(sprinkler::TIMER_SM); this->state_ = STARTING; } } else { // there is NOT another valve to run... this->fsm_transition_to_shutdown_(); } } void Sprinkler::fsm_transition_to_shutdown_() { this->state_ = STOPPING; this->set_timer_duration_(sprinkler::TIMER_SM, this->start_delay_ + this->stop_delay_ + this->switching_delay_.value_or(0) + 1); this->start_timer_(sprinkler::TIMER_SM); } std::string Sprinkler::req_as_str_(SprinklerValveRunRequestOrigin origin) { switch (origin) { case USER: return "USER"; case CYCLE: return "CYCLE"; case QUEUE: return "QUEUE"; default: return "UNKNOWN"; } } std::string Sprinkler::state_as_str_(SprinklerState state) { switch (state) { case IDLE: return "IDLE"; case STARTING: return "STARTING"; case ACTIVE: return "ACTIVE"; case STOPPING: return "STOPPING"; case BYPASS: return "BYPASS"; default: return "UNKNOWN"; } } void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) { if (this->timer_duration_(timer_index) > 0) { this->set_timeout(this->timer_[timer_index].name, this->timer_duration_(timer_index), this->timer_cbf_(timer_index)); this->timer_[timer_index].start_time = millis(); this->timer_[timer_index].active = true; } ESP_LOGVV(TAG, "Timer %zu started for %" PRIu32 " sec", static_cast(timer_index), this->timer_duration_(timer_index) / 1000); } bool Sprinkler::cancel_timer_(const SprinklerTimerIndex timer_index) { this->timer_[timer_index].active = false; return this->cancel_timeout(this->timer_[timer_index].name); } bool Sprinkler::timer_active_(const SprinklerTimerIndex timer_index) { return this->timer_[timer_index].active; } void Sprinkler::set_timer_duration_(const SprinklerTimerIndex timer_index, const uint32_t time) { this->timer_[timer_index].time = 1000 * time; } uint32_t Sprinkler::timer_duration_(const SprinklerTimerIndex timer_index) { return this->timer_[timer_index].time; } std::function Sprinkler::timer_cbf_(const SprinklerTimerIndex timer_index) { return this->timer_[timer_index].func; } void Sprinkler::valve_selection_callback_() { this->timer_[sprinkler::TIMER_VALVE_SELECTION].active = false; ESP_LOGVV(TAG, "Valve selection timer expired"); if (this->manual_valve_.has_value()) { this->fsm_request_(this->manual_valve_.value()); this->manual_valve_.reset(); } } void Sprinkler::sm_timer_callback_() { this->timer_[sprinkler::TIMER_SM].active = false; ESP_LOGVV(TAG, "State machine timer expired"); this->fsm_transition_(); } void Sprinkler::dump_config() { ESP_LOGCONFIG(TAG, "Sprinkler Controller -- %s", this->name_.c_str()); if (this->manual_selection_delay_.has_value()) { ESP_LOGCONFIG(TAG, " Manual Selection Delay: %" PRIu32 " seconds", this->manual_selection_delay_.value_or(0)); } if (this->repeat().has_value()) { ESP_LOGCONFIG(TAG, " Repeat Cycles: %" PRIu32 " times", this->repeat().value_or(0)); } if (this->start_delay_) { if (this->start_delay_is_valve_delay_) { ESP_LOGCONFIG(TAG, " Pump Start Valve Delay: %" PRIu32 " seconds", this->start_delay_); } else { ESP_LOGCONFIG(TAG, " Pump Start Pump Delay: %" PRIu32 " seconds", this->start_delay_); } } if (this->stop_delay_) { if (this->stop_delay_is_valve_delay_) { ESP_LOGCONFIG(TAG, " Pump Stop Valve Delay: %" PRIu32 " seconds", this->stop_delay_); } else { ESP_LOGCONFIG(TAG, " Pump Stop Pump Delay: %" PRIu32 " seconds", this->stop_delay_); } } if (this->switching_delay_.has_value()) { if (this->valve_overlap_) { ESP_LOGCONFIG(TAG, " Valve Overlap: %" PRIu32 " seconds", this->switching_delay_.value_or(0)); } else { ESP_LOGCONFIG(TAG, " Valve Open Delay: %" PRIu32 " seconds\n" " Pump Switch Off During Valve Open Delay: %s", this->switching_delay_.value_or(0), YESNO(this->pump_switch_off_during_valve_open_delay_)); } } for (size_t valve_number = 0; valve_number < this->number_of_valves(); valve_number++) { ESP_LOGCONFIG(TAG, " Valve %zu:\n" " Name: %s\n" " Run Duration: %" PRIu32 " seconds", valve_number, this->valve_name(valve_number), this->valve_run_duration(valve_number)); if (this->valve_[valve_number].valve_switch.pulse_duration()) { ESP_LOGCONFIG(TAG, " Pulse Duration: %" PRIu32 " milliseconds", this->valve_[valve_number].valve_switch.pulse_duration()); } } if (!this->pump_.empty()) { ESP_LOGCONFIG(TAG, " Total number of pumps: %zu", this->pump_.size()); } if (!this->valve_.empty()) { ESP_LOGCONFIG(TAG, " Total number of valves: %zu", this->valve_.size()); } } } // namespace sprinkler } // namespace esphome