From 6e554e7d6bc6d1582cd5b7c7a08281e46a8ad01d Mon Sep 17 00:00:00 2001 From: chl33 <64091421+chl33@users.noreply.github.com> Date: Sat, 19 Oct 2024 11:23:42 -0400 Subject: [PATCH] Limit number of pump doses in a watering cycle (#17) If there are too many pump doses in watering cycle, then switch to a "watering paused" state and wait for 12 hours. This is a safety features to avoid over-watering if something goes wrong, such as unreliable moisture sensor readings. --- lib/watering/watering.cpp | 40 +++++++++++++++++++++++++++---- lib/watering/watering.h | 7 +++++- lib/watering/watering_constants.h | 4 ++++ 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/lib/watering/watering.cpp b/lib/watering/watering.cpp index d2e0b05..5e36c44 100644 --- a/lib/watering/watering.cpp +++ b/lib/watering/watering.cpp @@ -35,6 +35,7 @@ const char* Watering::s_state_names[] = { "pump", // kStateDose "pump done", // kStateEndOfDose "soil is moist", // kStateWaitForNextCycle + "watering paused", // kStateWateringPaused "watering disabled", // kStateDisabled "pump test", // kStatePumpTest "test", // kStateTest @@ -46,6 +47,7 @@ int Watering::direction() const { case kStateEval: case kStateDose: case kStateEndOfDose: + case kStateWateringPaused: return +1; case kStateWaitForNextCycle: @@ -97,11 +99,13 @@ Watering::Watering(unsigned index, const char* name, uint8_t moisture_pin, uint8 m_between_doses_sec("between_doses_sec", kPumpOffSec, units::kSeconds, "Wait between doses", kCfgSet, 0, m_cfg_vg), m_state("watering_state", kStateWaitForNextCycle, "", "watering state", 0, m_vg), + m_sec_since_dose("sec_since_pump", 0, units::kSeconds, "seconds since pump dose", 0, 0, m_vg), + m_max_doses_per_cycle("max_doses_per_cycle", kMaxDosesPerCycle, "", "maximum doses per cycle", + kCfgSet, m_cfg_vg), + m_doses_this_cycle("doses_this_cycle", 0, "", "doses this cycle", 0, m_vg), m_watering_enabled("watering_enabled", false, "watering enabled", kCfgSet, m_cfg_vg), m_reservoir_check_enabled("res_check_enabled", false, "reservior check enabled", kCfgSet, - m_cfg_vg), - m_sec_since_dose("sec_since_pump", 0, units::kSeconds, "seconds since pump dose", 0, 0, - m_vg) { + m_cfg_vg) { setDependencies(&m_dependencies); // 10 seconds after boot, start the plant state machine. m_next_update_msec = millis() + (10 + 15 * index) * kMsecInSec; @@ -136,6 +140,9 @@ Watering::Watering(unsigned index, const char* name, uint8_t moisture_pin, uint8 return had->addMeas(json, m_sec_since_dose, ha::device_type::kSensor, ha::device_class::sensor::kDuration); }); + ha_discovery->addDiscoveryCallback([this](HADiscovery* had, JsonDocument* json) { + return had->addMeas(json, m_doses_this_cycle, ha::device_type::kSensor, nullptr); + }); } }); add_update_fn([this]() { loop(); }); @@ -248,10 +255,12 @@ void Watering::loop() { // Check whether to turn on the pump. const float val = m_moisture.filteredValue(); if (val > m_max_moisture_target.value()) { - // Moisure level is above the maximum threshold, so switch the a state + // Moisure level is above the maximum threshold, so switch to the state // where we wait for it to fall back below the minimum to start the // watering cycle again. setState(kStateWaitForNextCycle, kWaitForNextCycleMsec, "moisture past maximum range"); + } else if (m_doses_this_cycle.value() >= m_max_doses_per_cycle.value()) { + setState(kStateWateringPaused, kWaitForNextCycleMsec, "too many doses in cycle"); } else { setState(kStateDose, 1, "start pump"); } @@ -263,6 +272,7 @@ void Watering::loop() { case kStateDose: // Start the pump, and run for kPumpOnMsec. m_pump.turnOn(); + m_doses_this_cycle = m_doses_this_cycle.value() + 1; setState(kStateEndOfDose, m_pump_dose_msec.value(), "end watering dose"); break; @@ -276,10 +286,29 @@ void Watering::loop() { setState(kStateEval, kWaitForNextCycleMsec, "continue watering"); break; + case kStateWateringPaused: { + m_doses_this_cycle = 0; + const float val = m_moisture.filteredValue(); + if (val > m_max_moisture_target.value()) { + // Moisure level is above the maximum threshold, so switch to the state + // where we wait for it to fall back below the minimum to start the + // watering cycle again. + setState(kStateWaitForNextCycle, kWaitForNextCycleMsec, "moisture past maximum range"); + } else if (msecSincePump >= kWateringPauseMsec) { + // If the pause time expires, go back to eval state. + setState(kStateEval, 1, "re-enable watering after pause"); + } else { + // Otherwise stay in the paused state. + setState(kStateWateringPaused, kWaitForNextCycleMsec, ""); + } + break; + } + case kStateWaitForNextCycle: { // After moisture level reaches maximum level, wait for it to reach minimum // moisture level and also wait for minimum time between watering cycles, then // go back to kStateEval. + m_doses_this_cycle = 0; const float val = m_moisture.filteredValue(); if (val < m_min_moisture_target.value()) { setState(kStateEval, 1, "start watering"); @@ -292,6 +321,7 @@ void Watering::loop() { case kStateDisabled: // State for when pump is disabled. m_pump.turnOff(); + m_doses_this_cycle = 0; // Update every 10 seconds to get latest readings. setState(kStateDisabled, 10 * kMsecInSec, ""); break; @@ -299,11 +329,13 @@ void Watering::loop() { case kStatePumpTest: // State for when testing a single pump cycle, transitions to disabled mode. m_pump.turnOn(); + m_doses_this_cycle = 0; setState(kStateDisabled, m_pump_dose_msec.value(), "end of pump test"); break; case kStateTest: _fullTest(); + m_doses_this_cycle = 0; setState(kStateDisabled, kMsecInSec, "end of test"); break; } diff --git a/lib/watering/watering.h b/lib/watering/watering.h index 51ad63e..3595ae3 100644 --- a/lib/watering/watering.h +++ b/lib/watering/watering.h @@ -31,6 +31,9 @@ class Watering : public Module { // go back to kStateEval. kStateWaitForNextCycle, + // After too many does in a cycle, pause for 12 hours. + kStateWateringPaused, + // State for when pump is disabled. kStateDisabled, @@ -120,9 +123,11 @@ class Watering : public Module { FloatVariable m_pump_dose_msec; FloatVariable m_between_doses_sec; StateVariable m_state; + FloatVariable m_sec_since_dose; + Variable m_max_doses_per_cycle; + Variable m_doses_this_cycle; BoolVariable m_watering_enabled; BoolVariable m_reservoir_check_enabled; - FloatVariable m_sec_since_dose; }; } // namespace og3 diff --git a/lib/watering/watering_constants.h b/lib/watering/watering_constants.h index a768382..8f9859d 100644 --- a/lib/watering/watering_constants.h +++ b/lib/watering/watering_constants.h @@ -31,4 +31,8 @@ constexpr unsigned kFullMoistureCounts = 1470; // Default ADC reading at which to consider soil moisture to be 0%. constexpr unsigned kNoMoistureCounts = 2900; +constexpr unsigned kMaxDosesPerCycle = 5; + +constexpr unsigned kWateringPauseMsec = 12 * kMsecInHour; + } // namespace og3