ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
sprinkler.cpp
Go to the documentation of this file.
1#include "automation.h"
2#include "sprinkler.h"
3
6#include "esphome/core/log.h"
8#include <cinttypes>
9#include <utility>
10
11namespace esphome::sprinkler {
12
13ESPHOME_LOG_TAG(TAG, "sprinkler");
14
16 float value;
17 if (!this->restore_value_) {
18 value = this->initial_value_;
19 } else {
21 if (!this->pref_.load(&value)) {
22 if (!std::isnan(this->initial_value_)) {
23 value = this->initial_value_;
24 } else {
25 value = this->traits.get_min_value();
26 }
27 }
28 }
29 this->publish_state(value);
30}
31
33 this->set_trigger_.trigger(value);
34
35 this->publish_state(value);
36
37 if (this->restore_value_)
38 this->pref_.save(&value);
39}
40
41void SprinklerControllerNumber::dump_config() { LOG_NUMBER("", "Sprinkler Controller Number", this); }
42
44
46 // Loop is only enabled when f_ has a value (see setup())
47 auto s = (*this->f_)(); // NOLINT(bugprone-unchecked-optional-access)
48 if (s.has_value()) {
49 this->publish_state(*s);
50 }
51}
52
54 if (this->prev_trigger_ != nullptr) {
56 }
57
58 if (state) {
59 this->prev_trigger_ = &this->turn_on_trigger_;
61 } else {
62 this->prev_trigger_ = &this->turn_off_trigger_;
64 }
65
66 this->publish_state(state);
67}
68
69void SprinklerControllerSwitch::set_state_lambda(std::function<optional<bool>()> &&f) { this->f_ = f; }
71
73 this->state = this->get_initial_state_with_restore_mode().value_or(false);
74 // Disable loop if no state lambda is set - nothing to poll
75 if (!this->f_.has_value()) {
76 this->disable_loop();
77 }
78}
79
80void SprinklerControllerSwitch::dump_config() { LOG_SWITCH("", "Sprinkler Switch", this); }
81
84 : controller_(controller), valve_(valve) {}
85
87 // Use wrapping subtraction so 32-bit millis() rollover is handled correctly:
88 // (now - start) yields the true elapsed time even across the 49.7-day boundary.
90 switch (this->state_) {
91 case STARTING:
92 if ((now - *this->start_millis_) > this->start_delay_) { // NOLINT(bugprone-unchecked-optional-access)
93 this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state
94 }
95 break;
96
97 case ACTIVE:
98 if ((now - *this->start_millis_) > // NOLINT(bugprone-unchecked-optional-access)
99 (this->start_delay_ + this->run_duration_)) {
100 this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down
101 }
102 break;
103
104 case STOPPING:
105 if ((now - *this->stop_millis_) > this->stop_delay_) { // NOLINT(bugprone-unchecked-optional-access)
106 this->kill_(); // stop_delay_has been exceeded, ensure all valves are off
107 }
108 break;
109
110 default:
111 break;
112 }
113}
114
116 if (controller != nullptr) {
117 this->controller_ = controller;
118 }
119}
120
122 if (valve != nullptr) {
123 if (this->state_ != IDLE) { // Only kill if not already idle
124 this->kill_(); // ensure everything is off before we let go!
125 }
126 this->state_ = IDLE; // reset state
127 this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it
128 this->start_millis_.reset(); // reset because (new) valve has not been started yet
129 this->stop_millis_.reset(); // reset because (new) valve has not been started yet
130 this->valve_ = valve; // finally, set the pointer to the new valve
131 }
132}
133
135 if (run_duration) {
136 this->run_duration_ = run_duration * 1000;
137 }
138}
139
140void SprinklerValveOperator::set_start_delay(uint32_t start_delay, bool start_delay_is_valve_delay) {
141 this->start_delay_is_valve_delay_ = start_delay_is_valve_delay;
142 this->start_delay_ = start_delay * 1000; // because 1000 milliseconds is one second
143}
144
145void SprinklerValveOperator::set_stop_delay(uint32_t stop_delay, bool stop_delay_is_valve_delay) {
146 this->stop_delay_is_valve_delay_ = stop_delay_is_valve_delay;
147 this->stop_delay_ = stop_delay * 1000; // because 1000 milliseconds is one second
148}
149
151 if (!this->run_duration_) { // can't start if zero run duration
152 return;
153 }
154 if (this->start_delay_ && (this->pump_switch() != nullptr)) {
155 this->state_ = STARTING; // STARTING state requires both a pump and a start_delay_
156 if (this->start_delay_is_valve_delay_) {
157 this->pump_on_();
158 } else if (!this->pump_switch()->state) { // if the pump is already on, wait to switch on the valve
159 this->valve_on_(); // to ensure consistent run time
160 }
161 } else {
162 this->run_(); // there is no start_delay_, so just start the pump and valve
163 }
164 this->stop_millis_.reset();
165 this->start_millis_ = millis(); // save the time the start request was made
166}
167
169 if ((this->state_ == IDLE) || (this->state_ == STOPPING)) { // can't stop if already stopped or stopping
170 return;
171 }
172 if (this->stop_delay_ && (this->pump_switch() != nullptr)) {
173 this->state_ = STOPPING; // STOPPING state requires both a pump and a stop_delay_
174 if (this->stop_delay_is_valve_delay_) {
175 this->pump_off_();
176 } else {
177 this->valve_off_();
178 }
179 if (this->pump_switch()->state) { // if the pump is still on at this point, it may be in use...
180 this->valve_off_(); // ...so just switch the valve off now to ensure consistent run time
181 }
182 } else {
183 this->kill_(); // there is no stop_delay_, so just stop the pump and valve
184 }
185 this->stop_millis_ = millis(); // save the time the stop request was made
186}
187
189
191 if (!this->start_millis_.has_value()) {
192 return this->run_duration(); // hasn't been started yet
193 }
194
195 if (this->stop_millis_.has_value()) {
196 uint32_t elapsed = *this->stop_millis_ - *this->start_millis_;
197 if (elapsed >= this->start_delay_ + this->run_duration_) {
198 return 0; // valve was active for more than its configured duration, so we are done
199 }
200 if (elapsed <= this->start_delay_) {
201 return this->run_duration_ / 1000; // stopped during start delay, full run duration remains
202 }
203 return (this->run_duration_ - (elapsed - this->start_delay_)) / 1000;
204 }
205
206 uint32_t elapsed = millis() - *this->start_millis_;
207 uint32_t total_duration = this->start_delay_ + this->run_duration_;
208 if (elapsed < total_duration) {
209 return (total_duration - elapsed) / 1000; // running now
210 }
211 return 0; // run completed
212}
213
215 if ((this->controller_ == nullptr) || (this->valve_ == nullptr)) {
216 return nullptr;
217 }
218 if (this->valve_->pump_switch_index.has_value()) {
220 }
221 return nullptr;
222}
223
225 auto *pump = this->pump_switch();
226 if ((this->valve_ == nullptr) || (pump == nullptr)) { // safety first!
227 return;
228 }
229 if (this->controller_ == nullptr) { // safety first!
230 pump->turn_off(); // if no controller was set, just switch off the pump
231 } else { // ...otherwise, do it "safely"
232 auto state = this->state_; // this is silly, but...
233 this->state_ = BYPASS; // ...exclude me from the pump-in-use check that set_pump_state() does
234 this->controller_->set_pump_state(pump, false);
235 this->state_ = state;
236 }
237}
238
240 auto *pump = this->pump_switch();
241 if ((this->valve_ == nullptr) || (pump == nullptr)) { // safety first!
242 return;
243 }
244 if (this->controller_ == nullptr) { // safety first!
245 pump->turn_on(); // if no controller was set, just switch on the pump
246 } else { // ...otherwise, do it "safely"
247 auto state = this->state_; // this is silly, but...
248 this->state_ = BYPASS; // ...exclude me from the pump-in-use check that set_pump_state() does
249 this->controller_->set_pump_state(pump, true);
250 this->state_ = state;
251 }
252}
253
255 if ((this->valve_ == nullptr) || (this->valve_->valve_switch == nullptr)) { // safety first!
256 return;
257 }
258 if (this->valve_->valve_switch->state) {
259 this->valve_->valve_switch->turn_off();
260 }
261}
262
264 if ((this->valve_ == nullptr) || (this->valve_->valve_switch == nullptr)) { // safety first!
265 return;
266 }
267 if (!this->valve_->valve_switch->state) {
268 this->valve_->valve_switch->turn_on();
269 }
270}
271
273 this->state_ = IDLE;
274 this->valve_off_();
275 this->pump_off_();
276}
277
279 this->state_ = ACTIVE;
280 this->valve_on_();
281 this->pump_on_();
282}
283
286 SprinklerValveOperator *valve_op)
287 : valve_number_(valve_number), run_duration_(run_duration), valve_op_(valve_op) {}
288
289bool SprinklerValveRunRequest::has_valve_operator() { return !(this->valve_op_ == nullptr); }
290
292
293void SprinklerValveRunRequest::set_valve(size_t valve_number) {
294 this->valve_number_ = valve_number;
295 this->run_duration_ = 0;
296 this->valve_op_ = nullptr;
297 this->has_valve_ = true;
298}
299
301 if (valve_op != nullptr) {
302 this->valve_op_ = valve_op;
303 }
304}
305
307 this->has_valve_ = false;
308 this->origin_ = USER;
309 this->run_duration_ = 0;
310 this->valve_op_ = nullptr;
311}
312
314
316 if (this->has_valve_) {
317 return this->valve_number_;
318 }
319 return nullopt;
320}
321
323
325// The `name` is stored for dump_config logging; the scheduler keys each timer by its SprinklerTimerIndex
326Sprinkler::Sprinkler(const char *name)
327 : name_(name),
328 timer_{{{false, 0, 0, [this]() { this->sm_timer_callback_(); }},
329 {false, 0, 0, [this]() { this->valve_selection_callback_(); }}}} {}
330
331void Sprinkler::setup() {
332 this->all_valves_off_(true);
333 // Start with loop disabled - nothing to do when idle
334 this->disable_loop();
335}
336
337void Sprinkler::loop() {
338 for (auto &vo : this->valve_op_) {
339 vo.loop();
340 }
341 if (this->prev_req_.has_request()) {
342 if (this->prev_req_.has_valve_operator() && this->prev_req_.valve_operator()->state() == IDLE) {
343 this->prev_req_.reset();
344 }
345 } else if (this->state_ == IDLE) {
346 // Nothing more to do - disable loop until next activation
347 this->disable_loop();
348 }
349}
350
351void Sprinkler::add_valve(SprinklerControllerSwitch *valve_sw, SprinklerControllerSwitch *enable_sw) {
352 auto new_valve_number = this->number_of_valves();
353 this->valve_.resize(new_valve_number + 1);
354 SprinklerValve *new_valve = &this->valve_[new_valve_number];
355
356 new_valve->controller_switch = valve_sw;
357 new_valve->controller_switch->set_state_lambda([this, new_valve_number]() -> optional<bool> {
358 auto *valve = this->valve_switch(new_valve_number);
359 auto *pump = this->valve_pump_switch(new_valve_number);
360 if (valve == nullptr) {
361 return false;
362 }
363 if (pump != nullptr) {
364 return valve->state && pump->state;
365 }
366 return valve->state;
367 });
368
369 new_valve->valve_turn_off_automation =
370 make_unique<Automation<>>(new_valve->controller_switch->get_turn_off_trigger());
371 new_valve->valve_shutdown_action = make_unique<sprinkler::ShutdownAction<>>(this);
372 new_valve->valve_turn_off_automation->add_actions({new_valve->valve_shutdown_action.get()});
373
374 new_valve->valve_turn_on_automation = make_unique<Automation<>>(new_valve->controller_switch->get_turn_on_trigger());
375 new_valve->valve_resumeorstart_action = make_unique<sprinkler::StartSingleValveAction<>>(this);
376 new_valve->valve_resumeorstart_action->set_valve_to_start(new_valve_number);
377 new_valve->valve_turn_on_automation->add_actions({new_valve->valve_resumeorstart_action.get()});
378
379 if (enable_sw != nullptr) {
380 new_valve->enable_switch = enable_sw;
381 }
382}
383
384void Sprinkler::add_controller(Sprinkler *other_controller) { this->other_controllers_.push_back(other_controller); }
385
386void Sprinkler::set_controller_main_switch(SprinklerControllerSwitch *controller_switch) {
387 this->controller_sw_ = controller_switch;
388 controller_switch->set_state_lambda([this]() -> optional<bool> {
389 for (size_t valve_number = 0; valve_number < this->number_of_valves(); valve_number++) {
390 if (this->valve_[valve_number].controller_switch->state) {
391 return true;
392 }
393 }
394 return this->active_req_.has_request();
395 });
396
397 this->sprinkler_turn_off_automation_ = make_unique<Automation<>>(controller_switch->get_turn_off_trigger());
398 this->sprinkler_shutdown_action_ = make_unique<sprinkler::ShutdownAction<>>(this);
399 this->sprinkler_turn_off_automation_->add_actions({sprinkler_shutdown_action_.get()});
400
401 this->sprinkler_turn_on_automation_ = make_unique<Automation<>>(controller_switch->get_turn_on_trigger());
402 this->sprinkler_resumeorstart_action_ = make_unique<sprinkler::ResumeOrStartAction<>>(this);
403 this->sprinkler_turn_on_automation_->add_actions({sprinkler_resumeorstart_action_.get()});
404}
405
406void Sprinkler::set_controller_standby_switch(SprinklerControllerSwitch *standby_switch) {
407 this->standby_sw_ = standby_switch;
408
409 this->sprinkler_standby_turn_on_automation_ = make_unique<Automation<>>(standby_switch->get_turn_on_trigger());
410 this->sprinkler_standby_shutdown_action_ = make_unique<sprinkler::ShutdownAction<>>(this);
411 this->sprinkler_standby_turn_on_automation_->add_actions({sprinkler_standby_shutdown_action_.get()});
412}
413
414void Sprinkler::configure_valve_switch(size_t valve_number, switch_::Switch *valve_switch, uint32_t run_duration) {
415 if (this->is_a_valid_valve(valve_number)) {
416 this->valve_[valve_number].valve_switch = valve_switch;
417 this->valve_[valve_number].run_duration = run_duration;
418 }
419}
420
421void Sprinkler::configure_valve_pump_switch(size_t valve_number, switch_::Switch *pump_switch) {
422 if (this->is_a_valid_valve(valve_number)) {
423 for (size_t i = 0; i < this->pump_.size(); i++) { // check each existing registered pump
424 if (this->pump_[i] == pump_switch) { // if the "new" pump matches one we already have...
425 this->valve_[valve_number].pump_switch_index = i; // ...save its index in the pump vector...
426 return; // ...and we are done
427 }
428 } // if we end up here, no pumps matched, so add a new one
429 this->pump_.push_back(pump_switch);
430 this->valve_[valve_number].pump_switch_index = this->pump_.size() - 1; // save the index to the new pump
431 }
432}
433
434void Sprinkler::configure_valve_run_duration_number(size_t valve_number,
435 SprinklerControllerNumber *run_duration_number) {
436 if (this->is_a_valid_valve(valve_number)) {
437 this->valve_[valve_number].run_duration_number = run_duration_number;
438 }
439}
440
441void Sprinkler::set_divider(optional<uint32_t> divider) {
442 if (!divider.has_value()) {
443 return;
444 }
445 if (divider.value() > 0) {
446 this->set_multiplier(1.0 / divider.value());
447 this->set_repeat(divider.value() - 1);
448 } else if (divider.value() == 0) {
449 this->set_multiplier(1.0);
450 this->set_repeat(0);
451 }
452}
453
454void Sprinkler::set_multiplier(const optional<float> multiplier) {
455 if ((!multiplier.has_value()) || (multiplier.value() < 0)) {
456 return;
457 }
458 this->multiplier_ = multiplier.value();
459 if (this->multiplier_number_ == nullptr) {
460 return;
461 }
462 if (this->multiplier_number_->state == multiplier.value()) {
463 return;
464 }
465 auto call = this->multiplier_number_->make_call();
466 call.set_value(multiplier.value());
467 call.perform();
468}
469
470void Sprinkler::set_pump_start_delay(uint32_t start_delay) {
471 this->start_delay_is_valve_delay_ = false;
472 this->start_delay_ = start_delay;
473}
474
475void Sprinkler::set_pump_stop_delay(uint32_t stop_delay) {
476 this->stop_delay_is_valve_delay_ = false;
477 this->stop_delay_ = stop_delay;
478}
479
480void Sprinkler::set_valve_start_delay(uint32_t start_delay) {
481 this->start_delay_is_valve_delay_ = true;
482 this->start_delay_ = start_delay;
483}
484
485void Sprinkler::set_valve_stop_delay(uint32_t stop_delay) {
486 this->stop_delay_is_valve_delay_ = true;
487 this->stop_delay_ = stop_delay;
488}
489
490void Sprinkler::set_valve_open_delay(const uint32_t valve_open_delay) {
491 if (valve_open_delay > 0) {
492 this->valve_overlap_ = false;
493 this->switching_delay_ = valve_open_delay;
494 } else {
495 this->switching_delay_.reset();
496 }
497}
498
499void Sprinkler::set_valve_overlap(uint32_t valve_overlap) {
500 if (valve_overlap > 0) {
501 this->valve_overlap_ = true;
502 this->switching_delay_ = valve_overlap;
503 } else {
504 this->switching_delay_.reset();
505 }
506 this->pump_switch_off_during_valve_open_delay_ = false; // incompatible option
507}
508
509void Sprinkler::set_manual_selection_delay(uint32_t manual_selection_delay) {
510 if (manual_selection_delay > 0) {
511 this->manual_selection_delay_ = manual_selection_delay;
512 } else {
513 this->manual_selection_delay_.reset();
514 }
515}
516
517void Sprinkler::set_valve_run_duration(const optional<size_t> valve_number, const optional<uint32_t> run_duration) {
518 if (!valve_number.has_value() || !run_duration.has_value()) {
519 return;
520 }
521 if (!this->is_a_valid_valve(valve_number.value())) {
522 return;
523 }
524 this->valve_[valve_number.value()].run_duration = run_duration.value();
525 if (this->valve_[valve_number.value()].run_duration_number == nullptr) {
526 return;
527 }
528 if (this->valve_[valve_number.value()].run_duration_number->state == run_duration.value()) {
529 return;
530 }
531 auto call = this->valve_[valve_number.value()].run_duration_number->make_call();
532 if (this->valve_[valve_number.value()].run_duration_number->get_unit_of_measurement_ref() == MIN_STR) {
533 call.set_value(run_duration.value() / 60.0);
534 } else {
535 call.set_value(run_duration.value());
536 }
537 call.perform();
538}
539
540void Sprinkler::set_auto_advance(const bool auto_advance) {
541 if (this->auto_adv_sw_ == nullptr) {
542 return;
543 }
544 if (this->auto_adv_sw_->state == auto_advance) {
545 return;
546 }
547 this->auto_adv_sw_->control(auto_advance);
548}
549
550void Sprinkler::set_repeat(optional<uint32_t> repeat) {
551 this->target_repeats_ = repeat;
552 if (this->repeat_number_ == nullptr) {
553 return;
554 }
555 if (this->repeat_number_->state == repeat.value_or(0)) {
556 return;
557 }
558 auto call = this->repeat_number_->make_call();
559 call.set_value(repeat.value_or(0));
560 call.perform();
561}
562
563void Sprinkler::set_queue_enable(bool queue_enable) {
564 if (this->queue_enable_sw_ == nullptr) {
565 return;
566 }
567 if (this->queue_enable_sw_->state == queue_enable) {
568 return;
569 }
570 this->queue_enable_sw_->control(queue_enable);
571}
572
573void Sprinkler::set_reverse(const bool reverse) {
574 if (this->reverse_sw_ == nullptr) {
575 return;
576 }
577 if (this->reverse_sw_->state == reverse) {
578 return;
579 }
580 this->reverse_sw_->control(reverse);
581}
582
583void Sprinkler::set_standby(const bool standby) {
584 if (this->standby_sw_ == nullptr) {
585 return;
586 }
587 if (this->standby_sw_->state == standby) {
588 return;
589 }
590 this->standby_sw_->control(standby);
591}
592
593uint32_t Sprinkler::valve_run_duration(const size_t valve_number) {
594 if (!this->is_a_valid_valve(valve_number)) {
595 return 0;
596 }
597 if (this->valve_[valve_number].run_duration_number != nullptr) {
598 if (this->valve_[valve_number].run_duration_number->get_unit_of_measurement_ref() == MIN_STR) {
599 return static_cast<uint32_t>(roundf(this->valve_[valve_number].run_duration_number->state * 60));
600 } else {
601 return static_cast<uint32_t>(roundf(this->valve_[valve_number].run_duration_number->state));
602 }
603 }
604 return this->valve_[valve_number].run_duration;
605}
606
607uint32_t Sprinkler::valve_run_duration_adjusted(const size_t valve_number) {
608 uint32_t run_duration = 0;
609
610 if (this->is_a_valid_valve(valve_number)) {
611 run_duration = this->valve_run_duration(valve_number);
612 }
613 run_duration = static_cast<uint32_t>(roundf(run_duration * this->multiplier()));
614 // run_duration must not be less than any of these
615 if ((run_duration < this->start_delay_) || (run_duration < this->stop_delay_) ||
616 (run_duration < this->switching_delay_.value_or(0) * 2)) {
617 return std::max({this->switching_delay_.value_or(0) * 2, this->start_delay_, this->stop_delay_});
618 }
619 return run_duration;
620}
621
622bool Sprinkler::auto_advance() {
623 if (this->auto_adv_sw_ != nullptr) {
624 return this->auto_adv_sw_->state;
625 }
626 return true;
627}
628
629float Sprinkler::multiplier() {
630 if (this->multiplier_number_ != nullptr) {
631 return this->multiplier_number_->state;
632 }
633 return this->multiplier_;
634}
635
636optional<uint32_t> Sprinkler::repeat() {
637 if (this->repeat_number_ != nullptr) {
638 return static_cast<uint32_t>(roundf(this->repeat_number_->state));
639 }
640 return this->target_repeats_;
641}
642
643optional<uint32_t> Sprinkler::repeat_count() {
644 // if there is an active valve and auto-advance is enabled, we may be repeating, so return the count
645 if (this->active_req_.has_request() && this->auto_advance()) {
646 return this->repeat_count_;
647 }
648 return nullopt;
649}
650
651bool Sprinkler::queue_enabled() {
652 if (this->queue_enable_sw_ != nullptr) {
653 return this->queue_enable_sw_->state;
654 }
655 return true;
656}
657
658bool Sprinkler::reverse() {
659 if (this->reverse_sw_ != nullptr) {
660 return this->reverse_sw_->state;
661 }
662 return false;
663}
664
665bool Sprinkler::standby() {
666 if (this->standby_sw_ != nullptr) {
667 return this->standby_sw_->state;
668 }
669 return false;
670}
671
672void Sprinkler::start_from_queue() {
673 if (this->standby()) {
674 this->log_standby_warning_(LOG_STR("start_from_queue"));
675 return;
676 }
677 if (this->multiplier() == 0) {
678 this->log_multiplier_zero_warning_(LOG_STR("start_from_queue"));
679 return;
680 }
681 if (this->queued_valves_.empty()) {
682 return; // if there is nothing in the queue, don't do anything
683 }
684 if (this->queue_enabled() && this->active_valve().has_value()) {
685 return; // if there is already a valve running from the queue, do nothing
686 }
687
688 this->set_auto_advance(false);
689 this->set_queue_enable(true);
690
691 this->reset_cycle_states_(); // just in case auto-advance is switched on later
692 this->repeat_count_ = 0;
693 this->fsm_kick_(); // will automagically pick up from the queue (it has priority)
694}
695
696void Sprinkler::start_full_cycle() {
697 if (this->standby()) {
698 this->log_standby_warning_(LOG_STR("start_full_cycle"));
699 return;
700 }
701 if (this->multiplier() == 0) {
702 this->log_multiplier_zero_warning_(LOG_STR("start_full_cycle"));
703 return;
704 }
705 if (this->auto_advance() && this->active_valve().has_value()) {
706 return; // if auto-advance is already enabled and there is already a valve running, do nothing
707 }
708
709 this->set_queue_enable(false);
710
711 this->prep_full_cycle_();
712 this->repeat_count_ = 0;
713 // if there is no active valve already, start the first valve in the cycle
714 if (!this->active_req_.has_request()) {
715 this->fsm_kick_();
716 }
717}
718
719void Sprinkler::start_single_valve(const optional<size_t> valve_number, optional<uint32_t> run_duration) {
720 if (this->standby()) {
721 this->log_standby_warning_(LOG_STR("start_single_valve"));
722 return;
723 }
724 if (this->multiplier() == 0) {
725 this->log_multiplier_zero_warning_(LOG_STR("start_single_valve"));
726 return;
727 }
728 if (!valve_number.has_value() || (valve_number == this->active_valve())) {
729 return;
730 }
731
732 this->set_auto_advance(false);
733 this->set_queue_enable(false);
734
735 this->reset_cycle_states_(); // just in case auto-advance is switched on later
736 this->repeat_count_ = 0;
737 this->fsm_request_(valve_number.value(), run_duration.value_or(0));
738}
739
740void Sprinkler::queue_valve(optional<size_t> valve_number, optional<uint32_t> run_duration) {
741 if (valve_number.has_value()) {
742 if (this->is_a_valid_valve(valve_number.value()) && (this->queued_valves_.size() < this->max_queue_size_)) {
743 SprinklerQueueItem item{valve_number.value(), run_duration.value_or(0)};
744 this->queued_valves_.insert(this->queued_valves_.begin(), item);
745 ESP_LOGD(TAG, "Valve %zu placed into queue with run duration of %" PRIu32 " seconds", valve_number.value_or(0),
746 run_duration.value_or(0));
747 }
748 }
749}
750
751void Sprinkler::clear_queued_valves() {
752 this->queued_valves_.clear();
753 ESP_LOGD(TAG, "Queue cleared");
754}
755
756void Sprinkler::next_valve() {
757 if (this->standby()) {
758 this->log_standby_warning_(LOG_STR("next_valve"));
759 return;
760 }
761
762 if (this->state_ == IDLE) {
763 this->reset_cycle_states_(); // just in case auto-advance is switched on later
764 }
765
766 this->manual_valve_ = this->next_valve_number_(
767 this->manual_valve_.value_or(this->active_req_.valve_as_opt().value_or(this->number_of_valves() - 1)),
768 !this->next_prev_ignore_disabled_, true);
769
770 if (!this->manual_valve_.has_value()) {
771 ESP_LOGD(TAG, "next_valve was called but no valve could be started; perhaps next_prev_ignore_disabled allows only "
772 "enabled valves and no valves are enabled?");
773 return;
774 }
775
776 if (this->manual_selection_delay_.has_value()) {
777 this->set_timer_duration_(sprinkler::TIMER_VALVE_SELECTION, this->manual_selection_delay_.value());
778 this->start_timer_(sprinkler::TIMER_VALVE_SELECTION);
779 } else {
780 this->fsm_request_(this->manual_valve_.value());
781 }
782}
783
784void Sprinkler::previous_valve() {
785 if (this->standby()) {
786 this->log_standby_warning_(LOG_STR("previous_valve"));
787 return;
788 }
789
790 if (this->state_ == IDLE) {
791 this->reset_cycle_states_(); // just in case auto-advance is switched on later
792 }
793
794 this->manual_valve_ =
795 this->previous_valve_number_(this->manual_valve_.value_or(this->active_req_.valve_as_opt().value_or(0)),
796 !this->next_prev_ignore_disabled_, true);
797
798 if (!this->manual_valve_.has_value()) {
799 ESP_LOGD(TAG, "previous_valve was called but no valve could be started; perhaps next_prev_ignore_disabled allows "
800 "only enabled valves and no valves are enabled?");
801 return;
802 }
803
804 if (this->manual_selection_delay_.has_value()) {
805 this->set_timer_duration_(sprinkler::TIMER_VALVE_SELECTION, this->manual_selection_delay_.value());
806 this->start_timer_(sprinkler::TIMER_VALVE_SELECTION);
807 } else {
808 this->fsm_request_(this->manual_valve_.value());
809 }
810}
811
812void Sprinkler::shutdown(bool clear_queue) {
813 this->cancel_timer_(sprinkler::TIMER_VALVE_SELECTION);
814 this->active_req_.reset();
815 this->manual_valve_.reset();
816 this->next_req_.reset();
817 for (auto &vo : this->valve_op_) {
818 vo.stop();
819 }
820 this->fsm_transition_to_shutdown_();
821 if (clear_queue) {
822 this->clear_queued_valves();
823 this->repeat_count_ = 0;
824 }
825}
826
827void Sprinkler::pause() {
828 if (this->paused_valve_.has_value() || !this->active_req_.has_request()) {
829 return; // we can't pause if we're already paused or if there is no active valve
830 }
831 this->paused_valve_ = this->active_valve();
832 this->resume_duration_ = this->time_remaining_active_valve();
833 this->shutdown(false);
834 ESP_LOGD(TAG, "Paused valve %zu with %" PRIu32 " seconds remaining", this->paused_valve_.value_or(0),
835 this->resume_duration_.value_or(0));
836}
837
838void Sprinkler::resume() {
839 if (this->standby()) {
840 this->log_standby_warning_(LOG_STR("resume"));
841 return;
842 }
843
844 if (this->paused_valve_.has_value() && (this->resume_duration_.has_value())) {
845 const size_t paused_valve = *this->paused_valve_;
846 const uint32_t resume_duration = *this->resume_duration_;
847 // Resume only if valve has not been completed yet
848 if (!this->valve_cycle_complete_(paused_valve)) {
849 ESP_LOGD(TAG, "Resuming valve %zu with %" PRIu32 " seconds remaining", paused_valve, resume_duration);
850 this->fsm_request_(paused_valve, resume_duration);
851 }
852 this->reset_resume();
853 } else {
854 ESP_LOGD(TAG, "No valve to resume!");
855 }
856}
857
858void Sprinkler::resume_or_start_full_cycle() {
859 if (this->paused_valve_.has_value() && (this->resume_duration_.has_value())) {
860 this->resume();
861 } else {
862 this->start_full_cycle();
863 }
864}
865
866void Sprinkler::reset_resume() {
867 this->paused_valve_.reset();
868 this->resume_duration_.reset();
869}
870
871#pragma GCC diagnostic push
872#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
873const char *Sprinkler::valve_name(const size_t valve_number) {
874 if (this->is_a_valid_valve(valve_number)) {
875 return this->valve_[valve_number].controller_switch->get_name().c_str();
876 }
877 return nullptr;
878}
879#pragma GCC diagnostic pop
880
881const LogString *Sprinkler::valve_log_name(const size_t valve_number) {
882 if (this->is_a_valid_valve(valve_number)) {
883 return this->valve_[valve_number].controller_switch->get_log_name();
884 }
885 return nullptr;
886}
887
888optional<SprinklerValveRunRequestOrigin> Sprinkler::active_valve_request_is_from() {
889 if (this->active_req_.has_request()) {
890 return this->active_req_.request_is_from();
891 }
892 return nullopt;
893}
894
895optional<size_t> Sprinkler::active_valve() {
896 if (!this->valve_overlap_ && this->prev_req_.has_request() && this->prev_req_.has_valve_operator() &&
897 (this->prev_req_.valve_operator()->state() == STARTING || this->prev_req_.valve_operator()->state() == ACTIVE)) {
898 return this->prev_req_.valve_as_opt();
899 }
900 return this->active_req_.valve_as_opt();
901}
902
903optional<size_t> Sprinkler::queued_valve() {
904 if (!this->queued_valves_.empty()) {
905 return this->queued_valves_.back().valve_number;
906 }
907 return nullopt;
908}
909
910bool Sprinkler::is_a_valid_valve(const size_t valve_number) { return (valve_number < this->number_of_valves()); }
911
912bool Sprinkler::pump_in_use(switch_::Switch *pump_switch) {
913 if (pump_switch == nullptr) {
914 return false; // we can't do anything if there's nothing to check
915 }
916 // a pump must be considered "in use" if a (distribution) valve it supplies is active. this means:
917 // - at least one SprinklerValveOperator:
918 // - has a valve loaded that depends on this pump
919 // - is in a state that depends on the pump: (ACTIVE and _possibly_ STARTING/STOPPING)
920 // - if NO SprinklerValveOperator is active but there is a run request pending (active_req_.has_request()) and the
921 // controller state is STARTING, valve open delay is configured but NOT pump_switch_off_during_valve_open_delay_
922 for (auto &vo : this->valve_op_) { // first, check if any SprinklerValveOperator has a valve dependent on this pump
923 if ((vo.state() != BYPASS) && (vo.pump_switch() != nullptr)) {
924 // the SprinklerValveOperator is configured with a pump; now check if it is the pump of interest
925 if (vo.pump_switch() == pump_switch) {
926 // now if the SprinklerValveOperator has a pump and it is either ACTIVE, is STARTING with a valve delay or
927 // is STOPPING with a valve delay, its pump can be considered "in use", so just return indicating this now
928 if ((vo.state() == ACTIVE) ||
929 ((vo.state() == STARTING) && this->start_delay_ && this->start_delay_is_valve_delay_) ||
930 ((vo.state() == STOPPING) && this->stop_delay_ && this->stop_delay_is_valve_delay_)) {
931 return true;
932 }
933 }
934 }
935 } // if we end up here, no SprinklerValveOperator was in a "give-away" state indicating that the pump is in use...
936 if (!this->valve_overlap_ && !this->pump_switch_off_during_valve_open_delay_ && this->switching_delay_.has_value() &&
937 this->active_req_.has_request() && (this->state_ != STOPPING)) {
938 // ...the controller is configured to keep the pump on during a valve open delay, so just return
939 // whether or not the next valve shares the same pump
940 auto *valve_pump = this->valve_pump_switch(this->active_req_.valve());
941 if (valve_pump == nullptr) {
942 return false; // valve has no pump, so this pump isn't in use by it
943 }
944 return pump_switch == valve_pump;
945 }
946 return false;
947}
948
949void Sprinkler::set_pump_state(switch_::Switch *pump_switch, bool state) {
950 if (pump_switch == nullptr) {
951 return; // we can't do anything if there's nothing to check
952 }
953
954 bool hold_pump_on = false;
955
956 for (auto &controller : this->other_controllers_) { // check if the pump is in use by another controller
957 if (controller != this) { // dummy check
958 if (controller->pump_in_use(pump_switch)) {
959 hold_pump_on = true; // if another controller says it's using this pump, keep it on
960 }
961 }
962 }
963 if (hold_pump_on) {
964 ESP_LOGD(TAG, "Leaving pump on because another controller instance is using it");
965 }
966
967 if (state) { // ...and now we can set the new state of the switch
968 pump_switch->turn_on();
969 } else if (!hold_pump_on && !this->pump_in_use(pump_switch)) {
970 pump_switch->turn_off();
971 }
972}
973
974uint32_t Sprinkler::total_cycle_time_all_valves() {
975 uint32_t total_time_remaining = 0;
976
977 for (size_t valve = 0; valve < this->number_of_valves(); valve++) {
978 total_time_remaining += this->valve_run_duration_adjusted(valve);
979 }
980
981 if (this->valve_overlap_) {
982 total_time_remaining -= this->switching_delay_.value_or(0) * (this->number_of_valves() - 1);
983 } else {
984 total_time_remaining += this->switching_delay_.value_or(0) * (this->number_of_valves() - 1);
985 }
986
987 return total_time_remaining;
988}
989
990uint32_t Sprinkler::total_cycle_time_enabled_valves() {
991 uint32_t total_time_remaining = 0;
992 uint32_t valve_count = 0;
993
994 for (size_t valve = 0; valve < this->number_of_valves(); valve++) {
995 if (this->valve_is_enabled_(valve)) {
996 total_time_remaining += this->valve_run_duration_adjusted(valve);
997 valve_count++;
998 }
999 }
1000
1001 if (valve_count) {
1002 if (this->valve_overlap_) {
1003 total_time_remaining -= this->switching_delay_.value_or(0) * (valve_count - 1);
1004 } else {
1005 total_time_remaining += this->switching_delay_.value_or(0) * (valve_count - 1);
1006 }
1007 }
1008
1009 return total_time_remaining;
1010}
1011
1012uint32_t Sprinkler::total_cycle_time_enabled_incomplete_valves() {
1013 uint32_t total_time_remaining = 0;
1014 uint32_t enabled_valve_count = 0;
1015 uint32_t incomplete_valve_count = 0;
1016
1017 for (size_t valve = 0; valve < this->number_of_valves(); valve++) {
1018 if (this->valve_is_enabled_(valve)) {
1019 enabled_valve_count++;
1020 if (!this->valve_cycle_complete_(valve)) {
1021 auto active = this->active_valve();
1022 if (!active.has_value() || (valve != *active)) {
1023 total_time_remaining += this->valve_run_duration_adjusted(valve);
1024 incomplete_valve_count++;
1025 } else {
1026 // to get here, there must be an active valve and this valve must be equal to 'valve'
1027 if (this->active_req_.valve_operator() == nullptr) { // no SVO has been assigned yet so it hasn't started
1028 total_time_remaining += this->valve_run_duration_adjusted(valve);
1029 incomplete_valve_count++;
1030 }
1031 }
1032 }
1033 }
1034 }
1035
1036 if (incomplete_valve_count > 0 && incomplete_valve_count >= enabled_valve_count) {
1037 incomplete_valve_count--;
1038 }
1039 if (incomplete_valve_count) {
1040 if (this->valve_overlap_) {
1041 total_time_remaining -= this->switching_delay_.value_or(0) * incomplete_valve_count;
1042 } else {
1043 total_time_remaining += this->switching_delay_.value_or(0) * incomplete_valve_count;
1044 }
1045 }
1046
1047 return total_time_remaining;
1048}
1049
1050uint32_t Sprinkler::total_queue_time() {
1051 uint32_t total_time_remaining = 0;
1052 uint32_t valve_count = 0;
1053
1054 for (auto &valve : this->queued_valves_) {
1055 if (valve.run_duration) {
1056 total_time_remaining += valve.run_duration;
1057 } else {
1058 total_time_remaining += this->valve_run_duration_adjusted(valve.valve_number);
1059 }
1060 valve_count++;
1061 }
1062
1063 if (valve_count) {
1064 if (this->valve_overlap_) {
1065 total_time_remaining -= this->switching_delay_.value_or(0) * (valve_count - 1);
1066 } else {
1067 total_time_remaining += this->switching_delay_.value_or(0) * (valve_count - 1);
1068 }
1069 }
1070
1071 return total_time_remaining;
1072}
1073
1074optional<uint32_t> Sprinkler::time_remaining_active_valve() {
1075 if (this->active_req_.has_request()) { // first try to return the value based on active_req_...
1076 if (this->active_req_.valve_operator() != nullptr) {
1077 return this->active_req_.valve_operator()->time_remaining();
1078 }
1079 }
1080 if (this->prev_req_.has_request()) { // try to return the value based on prev_req_...
1081 if (this->prev_req_.valve_operator() != nullptr) {
1082 return this->prev_req_.valve_operator()->time_remaining();
1083 }
1084 }
1085 return nullopt;
1086}
1087
1088optional<uint32_t> Sprinkler::time_remaining_current_operation() {
1089 if (!this->time_remaining_active_valve().has_value() && this->state_ == IDLE) {
1090 return nullopt;
1091 }
1092
1093 auto total_time_remaining = this->time_remaining_active_valve().value_or(0);
1094 if (this->auto_advance()) {
1095 total_time_remaining += this->total_cycle_time_enabled_incomplete_valves();
1096 if (this->repeat().value_or(0) > 0) {
1097 total_time_remaining +=
1098 (this->total_cycle_time_enabled_valves() * (this->repeat().value_or(0) - this->repeat_count().value_or(0)));
1099 }
1100 }
1101
1102 if (this->queue_enabled()) {
1103 total_time_remaining += this->total_queue_time();
1104 }
1105 return total_time_remaining;
1106}
1107
1108bool Sprinkler::any_controller_is_active() {
1109 if (this->state_ != IDLE) {
1110 return true;
1111 }
1112
1113 for (auto &controller : this->other_controllers_) {
1114 if (controller != this) { // dummy check
1115 if (controller->controller_state() != IDLE) {
1116 return true;
1117 }
1118 }
1119 }
1120 return false;
1121}
1122
1123SprinklerControllerSwitch *Sprinkler::control_switch(size_t valve_number) {
1124 if (this->is_a_valid_valve(valve_number)) {
1125 return this->valve_[valve_number].controller_switch;
1126 }
1127 return nullptr;
1128}
1129
1130SprinklerControllerSwitch *Sprinkler::enable_switch(size_t valve_number) {
1131 if (this->is_a_valid_valve(valve_number)) {
1132 return this->valve_[valve_number].enable_switch;
1133 }
1134 return nullptr;
1135}
1136
1137switch_::Switch *Sprinkler::valve_switch(const size_t valve_number) {
1138 if (this->is_a_valid_valve(valve_number)) {
1139 return this->valve_[valve_number].valve_switch;
1140 }
1141 return nullptr;
1142}
1143
1144switch_::Switch *Sprinkler::valve_pump_switch(const size_t valve_number) {
1145 if (this->is_a_valid_valve(valve_number)) {
1146 auto idx = this->valve_[valve_number].pump_switch_index;
1147 if (idx.has_value()) {
1148 return this->pump_[*idx];
1149 }
1150 }
1151 return nullptr;
1152}
1153
1154switch_::Switch *Sprinkler::valve_pump_switch_by_pump_index(size_t pump_index) {
1155 if (pump_index < this->pump_.size()) {
1156 return this->pump_[pump_index];
1157 }
1158 return nullptr;
1159}
1160
1161bool Sprinkler::valve_is_enabled_(const size_t valve_number) {
1162 if (this->is_a_valid_valve(valve_number)) {
1163 if (this->valve_[valve_number].enable_switch != nullptr) {
1164 return this->valve_[valve_number].enable_switch->state;
1165 } else {
1166 return true;
1167 }
1168 }
1169 return false;
1170}
1171
1172void Sprinkler::mark_valve_cycle_complete_(const size_t valve_number) {
1173 if (this->is_a_valid_valve(valve_number)) {
1174 ESP_LOGD(TAG, "Marking valve %u complete", valve_number);
1175 this->valve_[valve_number].valve_cycle_complete = true;
1176 }
1177}
1178
1179bool Sprinkler::valve_cycle_complete_(const size_t valve_number) {
1180 if (this->is_a_valid_valve(valve_number)) {
1181 return this->valve_[valve_number].valve_cycle_complete;
1182 }
1183 return false;
1184}
1185
1186optional<size_t> Sprinkler::next_valve_number_(const optional<size_t> first_valve, const bool include_disabled,
1187 const bool include_complete) {
1188 auto valve = first_valve.value_or(0);
1189 size_t start = first_valve.has_value() ? 1 : 0;
1190
1191 if (!this->is_a_valid_valve(valve)) {
1192 valve = 0;
1193 }
1194
1195 for (size_t offset = start; offset < this->number_of_valves(); offset++) {
1196 auto valve_of_interest = valve + offset;
1197 if (!this->is_a_valid_valve(valve_of_interest)) {
1198 valve_of_interest -= this->number_of_valves();
1199 }
1200
1201 if ((this->valve_is_enabled_(valve_of_interest) || include_disabled) &&
1202 (!this->valve_cycle_complete_(valve_of_interest) || include_complete)) {
1203 return valve_of_interest;
1204 }
1205 }
1206 return nullopt;
1207}
1208
1209optional<size_t> Sprinkler::previous_valve_number_(const optional<size_t> first_valve, const bool include_disabled,
1210 const bool include_complete) {
1211 auto valve = first_valve.value_or(this->number_of_valves() - 1);
1212 size_t start = first_valve.has_value() ? 1 : 0;
1213
1214 if (!this->is_a_valid_valve(valve)) {
1215 valve = this->number_of_valves() - 1;
1216 }
1217
1218 for (size_t offset = start; offset < this->number_of_valves(); offset++) {
1219 auto valve_of_interest = valve - offset;
1220 if (!this->is_a_valid_valve(valve_of_interest)) {
1221 valve_of_interest += this->number_of_valves();
1222 }
1223
1224 if ((this->valve_is_enabled_(valve_of_interest) || include_disabled) &&
1225 (!this->valve_cycle_complete_(valve_of_interest) || include_complete)) {
1226 return valve_of_interest;
1227 }
1228 }
1229 return nullopt;
1230}
1231
1232optional<size_t> Sprinkler::next_valve_number_in_cycle_(const optional<size_t> first_valve) {
1233 if (this->reverse()) {
1234 return this->previous_valve_number_(first_valve, false, false);
1235 }
1236 return this->next_valve_number_(first_valve, false, false);
1237}
1238
1239void Sprinkler::load_next_valve_run_request_(const optional<size_t> first_valve) {
1240 if (this->active_req_.has_request()) {
1241 this->prev_req_ = this->active_req_;
1242 } else {
1243 this->prev_req_.reset();
1244 }
1245
1246 if (this->next_req_.has_request()) {
1247 if (!this->next_req_.run_duration()) { // ensure the run duration is set correctly for consumption later on
1248 this->next_req_.set_run_duration(this->valve_run_duration_adjusted(this->next_req_.valve()));
1249 }
1250 return; // there is already a request pending
1251 } else if (this->queue_enabled() && !this->queued_valves_.empty()) {
1252 this->next_req_.set_valve(this->queued_valves_.back().valve_number);
1253 this->next_req_.set_request_from(QUEUE);
1254 if (this->queued_valves_.back().run_duration) {
1255 this->next_req_.set_run_duration(this->queued_valves_.back().run_duration);
1256 this->queued_valves_.pop_back();
1257 } else if (this->multiplier()) {
1258 this->next_req_.set_run_duration(this->valve_run_duration_adjusted(this->queued_valves_.back().valve_number));
1259 this->queued_valves_.pop_back();
1260 } else {
1261 this->next_req_.reset();
1262 }
1263 } else if (this->auto_advance() && this->multiplier()) {
1264 if (this->next_valve_number_in_cycle_(first_valve).has_value()) {
1265 // if there is another valve to run as a part of a cycle, load that
1266 this->next_req_.set_valve(this->next_valve_number_in_cycle_(first_valve).value_or(0));
1267 this->next_req_.set_request_from(CYCLE);
1268 this->next_req_.set_run_duration(
1269 this->valve_run_duration_adjusted(this->next_valve_number_in_cycle_(first_valve).value_or(0)));
1270 } else if ((this->repeat_count_++ < this->repeat().value_or(0))) {
1271 ESP_LOGD(TAG, "Repeating - starting cycle %" PRIu32 " of %" PRIu32, this->repeat_count_ + 1,
1272 this->repeat().value_or(0) + 1);
1273 // if there are repeats remaining and no more valves were left in the cycle, start a new cycle
1274 this->prep_full_cycle_();
1275 if (this->next_valve_number_in_cycle_().has_value()) { // this should always succeed here, but just in case...
1276 this->next_req_.set_valve(this->next_valve_number_in_cycle_().value_or(0));
1277 this->next_req_.set_request_from(CYCLE);
1278 this->next_req_.set_run_duration(
1279 this->valve_run_duration_adjusted(this->next_valve_number_in_cycle_().value_or(0)));
1280 }
1281 }
1282 }
1283}
1284
1285bool Sprinkler::any_valve_is_enabled_() {
1286 for (size_t valve_number = 0; valve_number < this->number_of_valves(); valve_number++) {
1287 if (this->valve_is_enabled_(valve_number))
1288 return true;
1289 }
1290 return false;
1291}
1292
1293void Sprinkler::start_valve_(SprinklerValveRunRequest *req) {
1294 if (!req->has_request()) {
1295 return; // we can't do anything if the request contains nothing
1296 }
1297 if (!this->is_a_valid_valve(req->valve())) {
1298 return; // we can't do anything if the valve number isn't valid
1299 }
1300 // Enable loop to monitor valve operator states
1301 this->enable_loop();
1302 for (auto &vo : this->valve_op_) { // find the first available SprinklerValveOperator, load it and start it up
1303 if (vo.state() == IDLE) {
1304 auto run_duration = req->run_duration() ? req->run_duration() : this->valve_run_duration_adjusted(req->valve());
1305 ESP_LOGD(TAG, "%s is starting valve %zu for %" PRIu32 " seconds, cycle %" PRIu32 " of %" PRIu32,
1306 LOG_STR_ARG(this->req_as_str_(req->request_is_from())), req->valve(), run_duration,
1307 this->repeat_count_ + 1, this->repeat().value_or(0) + 1);
1308 req->set_valve_operator(&vo);
1309 vo.set_controller(this);
1310 vo.set_valve(&this->valve_[req->valve()]);
1311 vo.set_run_duration(run_duration);
1312 vo.set_start_delay(this->start_delay_, this->start_delay_is_valve_delay_);
1313 vo.set_stop_delay(this->stop_delay_, this->stop_delay_is_valve_delay_);
1314 vo.start();
1315 return;
1316 }
1317 }
1318}
1319
1320void Sprinkler::all_valves_off_(const bool include_pump) {
1321 for (size_t valve_index = 0; valve_index < this->number_of_valves(); valve_index++) {
1322 auto *valve_sw = this->valve_[valve_index].valve_switch;
1323 if ((valve_sw != nullptr) && valve_sw->state) {
1324 valve_sw->turn_off();
1325 }
1326 if (include_pump) {
1327 this->set_pump_state(this->valve_pump_switch(valve_index), false);
1328 }
1329 }
1330 ESP_LOGD(TAG, "All valves stopped%s", include_pump ? LOG_STR_LITERAL(", including pumps") : "");
1331}
1332
1333void Sprinkler::prep_full_cycle_() {
1334 this->set_auto_advance(true);
1335
1336 if (!this->any_valve_is_enabled_()) {
1337 for (auto &valve : this->valve_) {
1338 if (valve.enable_switch != nullptr) {
1339 if (!valve.enable_switch->state) {
1340 valve.enable_switch->turn_on();
1341 }
1342 }
1343 }
1344 }
1345 this->reset_cycle_states_();
1346}
1347
1348void Sprinkler::reset_cycle_states_() {
1349 for (auto &valve : this->valve_) {
1350 valve.valve_cycle_complete = false;
1351 }
1352}
1353
1354void Sprinkler::fsm_request_(size_t requested_valve, uint32_t requested_run_duration) {
1355 this->next_req_.set_valve(requested_valve);
1356 this->next_req_.set_run_duration(requested_run_duration);
1357 // if state is IDLE or ACTIVE, call fsm_transition_() to start it immediately;
1358 // otherwise, fsm_transition() will pick up next_req_ at the next appropriate transition
1359 this->fsm_kick_();
1360}
1361
1362void Sprinkler::fsm_kick_() {
1363 if ((this->state_ == IDLE) || (this->state_ == ACTIVE)) {
1364 this->fsm_transition_();
1365 }
1366}
1367
1368void Sprinkler::fsm_transition_() {
1369 ESP_LOGVV(TAG, "fsm_transition_ called; state is %s", LOG_STR_ARG(this->state_as_str_(this->state_)));
1370 switch (this->state_) {
1371 case IDLE: // the system was off -> start it up
1372 // advances to ACTIVE
1373 this->fsm_transition_from_shutdown_();
1374 break;
1375
1376 case ACTIVE:
1377 // advances to STOPPING or ACTIVE (again)
1378 this->fsm_transition_from_valve_run_();
1379 break;
1380
1381 case STARTING: {
1382 // follows valve open delay interval
1383 uint32_t timer_duration = this->active_req_.run_duration();
1384 if (timer_duration > this->switching_delay_.value_or(0)) {
1385 timer_duration -= this->switching_delay_.value_or(0);
1386 }
1387 this->set_timer_duration_(sprinkler::TIMER_SM, timer_duration);
1388 this->start_timer_(sprinkler::TIMER_SM);
1389 this->start_valve_(&this->active_req_);
1390 this->state_ = ACTIVE;
1391 if (this->next_req_.has_request()) {
1392 // another valve has been requested, so restart the timer so we pick it up quickly
1393 this->set_timer_duration_(sprinkler::TIMER_SM, this->manual_selection_delay_.value_or(1));
1394 this->start_timer_(sprinkler::TIMER_SM);
1395 }
1396 break;
1397 }
1398
1399 case STOPPING:
1400 // stop_delay_ has elapsed so just shut everything off
1401 this->active_req_.reset();
1402 this->manual_valve_.reset();
1403 this->all_valves_off_(true);
1404 this->state_ = IDLE;
1405 break;
1406
1407 default:
1408 break;
1409 }
1410 if (this->next_req_.has_request() && (this->state_ == IDLE)) {
1411 // another valve has been requested, so restart the timer so we pick it up quickly
1412 this->set_timer_duration_(sprinkler::TIMER_SM, this->manual_selection_delay_.value_or(1));
1413 this->start_timer_(sprinkler::TIMER_SM);
1414 }
1415 ESP_LOGVV(TAG, "fsm_transition_ complete; new state is %s", LOG_STR_ARG(this->state_as_str_(this->state_)));
1416}
1417
1418void Sprinkler::fsm_transition_from_shutdown_() {
1419 this->load_next_valve_run_request_();
1420
1421 if (this->next_req_.has_request()) { // there is a valve to run...
1422 this->active_req_.set_valve(this->next_req_.valve());
1423 this->active_req_.set_request_from(this->next_req_.request_is_from());
1424 this->active_req_.set_run_duration(this->next_req_.run_duration());
1425 this->next_req_.reset();
1426
1427 uint32_t timer_duration = this->active_req_.run_duration();
1428 if (timer_duration > this->switching_delay_.value_or(0)) {
1429 timer_duration -= this->switching_delay_.value_or(0);
1430 }
1431 this->set_timer_duration_(sprinkler::TIMER_SM, timer_duration);
1432 this->start_timer_(sprinkler::TIMER_SM);
1433 this->start_valve_(&this->active_req_);
1434 this->state_ = ACTIVE;
1435 }
1436}
1437
1438void Sprinkler::fsm_transition_from_valve_run_() {
1439 if (!this->active_req_.has_request()) { // dummy check...
1440 this->fsm_transition_to_shutdown_();
1441 return;
1442 }
1443
1444 if (!this->timer_active_(sprinkler::TIMER_SM)) { // only flag the valve as "complete" if the timer finished
1445 if ((this->active_req_.request_is_from() == CYCLE) || (this->active_req_.request_is_from() == USER)) {
1446 this->mark_valve_cycle_complete_(this->active_req_.valve());
1447 }
1448 } else {
1449 ESP_LOGD(TAG, "Valve cycle interrupted - NOT flagging valve as complete and stopping current valve");
1450 for (auto &vo : this->valve_op_) {
1451 vo.stop();
1452 }
1453 }
1454
1455 this->load_next_valve_run_request_(this->active_req_.valve());
1456
1457 if (this->next_req_.has_request()) { // there is another valve to run...
1458 auto *active_pump = this->valve_pump_switch(this->active_req_.valve());
1459 auto *next_pump = this->valve_pump_switch(this->next_req_.valve());
1460 bool same_pump = (active_pump != nullptr) && (next_pump != nullptr) && (active_pump == next_pump);
1461
1462 this->active_req_.set_valve(this->next_req_.valve());
1463 this->active_req_.set_request_from(this->next_req_.request_is_from());
1464 this->active_req_.set_run_duration(this->next_req_.run_duration());
1465 this->next_req_.reset();
1466
1467 // this->state_ = ACTIVE; // state isn't changing
1468 if (this->valve_overlap_ || !this->switching_delay_.has_value()) {
1469 uint32_t timer_duration = this->active_req_.run_duration();
1470 if (timer_duration > this->switching_delay_.value_or(0)) {
1471 timer_duration -= this->switching_delay_.value_or(0);
1472 }
1473 this->set_timer_duration_(sprinkler::TIMER_SM, timer_duration);
1474 this->start_timer_(sprinkler::TIMER_SM);
1475 this->start_valve_(&this->active_req_);
1476 } else {
1477 this->set_timer_duration_(
1479 this->switching_delay_.value() * 2 +
1480 (this->pump_switch_off_during_valve_open_delay_ && same_pump ? this->stop_delay_ : 0));
1481 this->start_timer_(sprinkler::TIMER_SM);
1482 this->state_ = STARTING;
1483 }
1484 } else { // there is NOT another valve to run...
1485 this->fsm_transition_to_shutdown_();
1486 }
1487}
1488
1489void Sprinkler::fsm_transition_to_shutdown_() {
1490 this->state_ = STOPPING;
1491 this->set_timer_duration_(sprinkler::TIMER_SM,
1492 this->start_delay_ + this->stop_delay_ + this->switching_delay_.value_or(0) + 1);
1493 this->start_timer_(sprinkler::TIMER_SM);
1494}
1495
1496void Sprinkler::log_standby_warning_(const LogString *method_name) {
1497 ESP_LOGW(TAG, "%s called but standby is enabled; no action taken", LOG_STR_ARG(method_name));
1498}
1499
1500void Sprinkler::log_multiplier_zero_warning_(const LogString *method_name) {
1501 ESP_LOGW(TAG, "%s called but multiplier is set to zero; no action taken", LOG_STR_ARG(method_name));
1502}
1503
1504// Request origin strings indexed by SprinklerValveRunRequestOrigin enum (0-2): USER, CYCLE, QUEUE
1505PROGMEM_STRING_TABLE(SprinklerRequestOriginStrings, "USER", "CYCLE", "QUEUE", "UNKNOWN");
1506
1507const LogString *Sprinkler::req_as_str_(SprinklerValveRunRequestOrigin origin) {
1508 return SprinklerRequestOriginStrings::get_log_str(static_cast<uint8_t>(origin),
1509 SprinklerRequestOriginStrings::LAST_INDEX);
1510}
1511
1512// Sprinkler state strings indexed by SprinklerState enum (0-4): IDLE, STARTING, ACTIVE, STOPPING, BYPASS
1513PROGMEM_STRING_TABLE(SprinklerStateStrings, "IDLE", "STARTING", "ACTIVE", "STOPPING", "BYPASS", "UNKNOWN");
1514
1515const LogString *Sprinkler::state_as_str_(SprinklerState state) {
1516 return SprinklerStateStrings::get_log_str(static_cast<uint8_t>(state), SprinklerStateStrings::LAST_INDEX);
1517}
1518
1519void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) {
1520 if (this->timer_duration_(timer_index) > 0) {
1521 this->set_timeout(static_cast<uint32_t>(timer_index), this->timer_duration_(timer_index),
1522 this->timer_cbf_(timer_index));
1523 this->timer_[timer_index].start_time = millis();
1524 this->timer_[timer_index].active = true;
1525 }
1526 ESP_LOGVV(TAG, "Timer %zu started for %" PRIu32 " sec", static_cast<size_t>(timer_index),
1527 this->timer_duration_(timer_index) / 1000);
1528}
1529
1530bool Sprinkler::cancel_timer_(const SprinklerTimerIndex timer_index) {
1531 this->timer_[timer_index].active = false;
1532 return this->cancel_timeout(static_cast<uint32_t>(timer_index));
1533}
1534
1535bool Sprinkler::timer_active_(const SprinklerTimerIndex timer_index) { return this->timer_[timer_index].active; }
1536
1537void Sprinkler::set_timer_duration_(const SprinklerTimerIndex timer_index, const uint32_t time) {
1538 this->timer_[timer_index].time = 1000 * time;
1539}
1540
1541uint32_t Sprinkler::timer_duration_(const SprinklerTimerIndex timer_index) { return this->timer_[timer_index].time; }
1542
1543std::function<void()> Sprinkler::timer_cbf_(const SprinklerTimerIndex timer_index) {
1544 return this->timer_[timer_index].func;
1545}
1546
1547void Sprinkler::valve_selection_callback_() {
1548 this->timer_[sprinkler::TIMER_VALVE_SELECTION].active = false;
1549 ESP_LOGVV(TAG, "Valve selection timer expired");
1550 if (this->manual_valve_.has_value()) {
1551 this->fsm_request_(this->manual_valve_.value());
1552 this->manual_valve_.reset();
1553 }
1554}
1555
1556void Sprinkler::sm_timer_callback_() {
1557 this->timer_[sprinkler::TIMER_SM].active = false;
1558 ESP_LOGVV(TAG, "State machine timer expired");
1559 this->fsm_transition_();
1560}
1561
1562void Sprinkler::dump_config() {
1563 ESP_LOGCONFIG(TAG, "Sprinkler Controller -- %s", this->name_);
1564 if (this->manual_selection_delay_.has_value()) {
1565 ESP_LOGCONFIG(TAG, " Manual Selection Delay: %" PRIu32 " seconds", this->manual_selection_delay_.value_or(0));
1566 }
1567 if (this->repeat().has_value()) {
1568 ESP_LOGCONFIG(TAG, " Repeat Cycles: %" PRIu32 " times", this->repeat().value_or(0));
1569 }
1570 if (this->start_delay_) {
1571 if (this->start_delay_is_valve_delay_) {
1572 ESP_LOGCONFIG(TAG, " Pump Start Valve Delay: %" PRIu32 " seconds", this->start_delay_);
1573 } else {
1574 ESP_LOGCONFIG(TAG, " Pump Start Pump Delay: %" PRIu32 " seconds", this->start_delay_);
1575 }
1576 }
1577 if (this->stop_delay_) {
1578 if (this->stop_delay_is_valve_delay_) {
1579 ESP_LOGCONFIG(TAG, " Pump Stop Valve Delay: %" PRIu32 " seconds", this->stop_delay_);
1580 } else {
1581 ESP_LOGCONFIG(TAG, " Pump Stop Pump Delay: %" PRIu32 " seconds", this->stop_delay_);
1582 }
1583 }
1584 if (this->switching_delay_.has_value()) {
1585 if (this->valve_overlap_) {
1586 ESP_LOGCONFIG(TAG, " Valve Overlap: %" PRIu32 " seconds", this->switching_delay_.value_or(0));
1587 } else {
1588 ESP_LOGCONFIG(TAG,
1589 " Valve Open Delay: %" PRIu32 " seconds\n"
1590 " Pump Switch Off During Valve Open Delay: %s",
1591 this->switching_delay_.value_or(0), YESNO(this->pump_switch_off_during_valve_open_delay_));
1592 }
1593 }
1594 for (size_t valve_number = 0; valve_number < this->number_of_valves(); valve_number++) {
1595 ESP_LOGCONFIG(TAG,
1596 " Valve %zu:\n"
1597 " Name: %s\n"
1598 " Run Duration: %" PRIu32 " seconds",
1599 valve_number, LOG_STR_ARG(this->valve_log_name(valve_number)),
1600 this->valve_run_duration(valve_number));
1601 }
1602 if (!this->pump_.empty()) {
1603 ESP_LOGCONFIG(TAG, " Total number of pumps: %zu", this->pump_.size());
1604 }
1605 if (!this->valve_.empty()) {
1606 ESP_LOGCONFIG(TAG, " Total number of valves: %zu", this->valve_.size());
1607 }
1608}
1609
1610} // namespace esphome::sprinkler
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void disable_loop()
Disable this component's loop.
ESPPreferenceObject make_entity_preference(uint32_t version=0)
Create a preference object for storing this entity's state/settings.
void stop_action()
Stop any action connected to this trigger.
Definition automation.h:564
void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE
Inform the parent automation that the event has triggered.
Definition automation.h:556
void publish_state(float state)
Definition number.cpp:20
NumberTraits traits
Definition number.h:41
void set_state_lambda(std::function< optional< bool >()> &&f)
Definition sprinkler.cpp:69
optional< std::function< optional< bool >()> > f_
Definition sprinkler.h:109
void set_pump_state(switch_::Switch *pump_switch, bool state)
switches on/off a pump "safely" by checking that the new state will not conflict with another control...
switch_::Switch * valve_pump_switch_by_pump_index(size_t pump_index)
returns a pointer to a valve's pump switch object
void valve_selection_callback_()
callback functions for timers
void set_valve(SprinklerValve *valve)
void set_run_duration(uint32_t run_duration)
void set_start_delay(uint32_t start_delay, bool start_delay_is_valve_delay)
void set_controller(Sprinkler *controller)
void set_stop_delay(uint32_t stop_delay, bool stop_delay_is_valve_delay)
SprinklerValveRunRequestOrigin origin_
Definition sprinkler.h:173
SprinklerValveRunRequestOrigin request_is_from()
Definition sprinkler.h:166
SprinklerValveOperator * valve_operator()
void set_run_duration(uint32_t run_duration)
void set_valve_operator(SprinklerValveOperator *valve_op)
Base class for all switches.
Definition switch.h:43
void turn_on()
Turn this switch on.
Definition switch.cpp:19
void turn_off()
Turn this switch off.
Definition switch.cpp:23
bool state
The current reported state of the binary sensor.
Definition switch.h:60
void publish_state(bool state)
Publish a state to the front-end from the back-end.
Definition switch.cpp:70
optional< bool > get_initial_state_with_restore_mode()
Returns the initial state of the switch, after applying restore mode rules.
Definition switch.cpp:49
bool state
Definition fan.h:2
constexpr float HARDWARE
For components that deal with hardware and are very important like GPIO switch.
Definition component.h:43
constexpr const char * MIN_STR
Definition sprinkler.h:15
ESPHOME_LOG_TAG(TAG, "sprinkler")
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
std::unique_ptr< StartSingleValveAction<> > valve_resumeorstart_action
Definition sprinkler.h:68
std::unique_ptr< Automation<> > valve_turn_off_automation
Definition sprinkler.h:69
SprinklerControllerSwitch * enable_switch
Definition sprinkler.h:62
std::unique_ptr< ShutdownAction<> > valve_shutdown_action
Definition sprinkler.h:67
SprinklerControllerSwitch * controller_switch
Definition sprinkler.h:61
std::unique_ptr< Automation<> > valve_turn_on_automation
Definition sprinkler.h:70
optional< size_t > pump_switch_index
Definition sprinkler.h:65