feat: add fertilizer cooldown functionality with web UI, HAL integration, and configuration support

This commit is contained in:
2026-04-30 22:09:04 +02:00
parent 6809a37d9d
commit db0f7daa4c
8 changed files with 63 additions and 6 deletions

View File

@@ -130,6 +130,7 @@ pub struct PlantConfig {
pub max_pump_current_ma: u16, pub max_pump_current_ma: u16,
pub ignore_current_error: bool, pub ignore_current_error: bool,
pub fertilizer_s: u16, pub fertilizer_s: u16,
pub fertilizer_cooldown_min: u16,
} }
impl Default for PlantConfig { impl Default for PlantConfig {
@@ -152,6 +153,7 @@ impl Default for PlantConfig {
max_pump_current_ma: 3000, max_pump_current_ma: 3000,
ignore_current_error: true, ignore_current_error: true,
fertilizer_s: 0, fertilizer_s: 0,
fertilizer_cooldown_min: 1440, // 1 day default
} }
} }
} }

View File

@@ -51,6 +51,8 @@ static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT];
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))] #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut CONSECUTIVE_WATERING_PLANT: [u32; PLANT_COUNT] = [0; PLANT_COUNT]; static mut CONSECUTIVE_WATERING_PLANT: [u32; PLANT_COUNT] = [0; PLANT_COUNT];
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))] #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut LAST_FERTILIZER_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT];
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut LOW_VOLTAGE_DETECTED: i8 = 0; static mut LOW_VOLTAGE_DETECTED: i8 = 0;
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))] #[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut RESTART_TO_CONF: i8 = 0; static mut RESTART_TO_CONF: i8 = 0;
@@ -342,6 +344,14 @@ impl Esp<'_> {
LAST_WATERING_TIMESTAMP[plant] = time.timestamp_millis(); LAST_WATERING_TIMESTAMP[plant] = time.timestamp_millis();
} }
} }
pub(crate) fn last_fertilizer_time(&self, plant: usize) -> i64 {
unsafe { LAST_FERTILIZER_TIMESTAMP[plant] }
}
pub(crate) fn store_last_fertilizer_time(&mut self, plant: usize, time: DateTime<Utc>) {
unsafe {
LAST_FERTILIZER_TIMESTAMP[plant] = time.timestamp_millis();
}
}
pub(crate) fn set_low_voltage_in_cycle(&mut self) { pub(crate) fn set_low_voltage_in_cycle(&mut self) {
unsafe { unsafe {
LOW_VOLTAGE_DETECTED = 1; LOW_VOLTAGE_DETECTED = 1;

View File

@@ -311,6 +311,10 @@ pub enum LogMessage {
PumpOpenLoopCurrent, PumpOpenLoopCurrent,
#[strum(serialize = "Pump Open current sensor required but did not work: ${number_a}")] #[strum(serialize = "Pump Open current sensor required but did not work: ${number_a}")]
PumpMissingSensorCurrent, PumpMissingSensorCurrent,
#[strum(
serialize = "Fertilizer applied for ${number_a}s on plant ${number_b} (last application ${txt_short} minutes ago)"
)]
FertilizerApplied,
#[strum(serialize = "MPPT Current sensor could not be reached")] #[strum(serialize = "MPPT Current sensor could not be reached")]
MPPTError, MPPTError,
#[strum( #[strum(

View File

@@ -715,13 +715,33 @@ pub async fn do_secure_pump(
let mut pump_time_ms: u32 = 0; let mut pump_time_ms: u32 = 0;
if !dry_run { if !dry_run {
// Run fertilizer pump first if configured // Run fertilizer pump first if configured and not in cooldown
if plant_config.fertilizer_s > 0 { if plant_config.fertilizer_s > 0 {
info!("Starting fertilizer pump for {} seconds", plant_config.fertilizer_s); let current_time = board.board_hal.get_time().await;
let last_fertilizer = board.board_hal.get_esp().last_fertilizer_time(plant_id);
let elapsed_minutes = (current_time.timestamp() - last_fertilizer) / 60;
if elapsed_minutes >= plant_config.fertilizer_cooldown_min as i64 {
info!("Starting fertilizer pump for {} seconds (last fertilizer was {} minutes ago)",
plant_config.fertilizer_s, elapsed_minutes);
log(
LogMessage::FertilizerApplied,
plant_config.fertilizer_s as u32,
(plant_id + 1) as u32,
&elapsed_minutes.to_string(),
"",
);
board.board_hal.extra2(true).await?; board.board_hal.extra2(true).await?;
Timer::after_millis(plant_config.fertilizer_s as u64 * 1000).await; Timer::after_millis(plant_config.fertilizer_s as u64 * 1000).await;
board.board_hal.extra2(false).await?; board.board_hal.extra2(false).await?;
info!("Fertilizer pump stopped"); info!("Fertilizer pump stopped");
// Store the current time as last fertilizer time
board.board_hal.get_esp().store_last_fertilizer_time(plant_id, current_time);
} else {
let remaining_minutes = plant_config.fertilizer_cooldown_min as i64 - elapsed_minutes;
info!("Skipping fertilizer (cooldown: {} minutes remaining)", remaining_minutes);
}
} }
board.board_hal.get_tank_sensor()?.reset_flow_meter(); board.board_hal.get_tank_sensor()?.reset_flow_meter();

View File

@@ -89,6 +89,8 @@ pub struct PlantState {
pub sensor_a_firmware_build_minutes: Option<u32>, pub sensor_a_firmware_build_minutes: Option<u32>,
/// Last known firmware build timestamp for sensor B. /// Last known firmware build timestamp for sensor B.
pub sensor_b_firmware_build_minutes: Option<u32>, pub sensor_b_firmware_build_minutes: Option<u32>,
/// Last time fertilizer was applied (Unix timestamp in seconds).
pub last_fertilizer_time: i64,
} }
fn map_range_moisture( fn map_range_moisture(
@@ -162,6 +164,7 @@ impl PlantState {
let previous_pump = board.board_hal.get_esp().last_pump_time(plant_id); let previous_pump = board.board_hal.get_esp().last_pump_time(plant_id);
let consecutive_pump_count = board.board_hal.get_esp().consecutive_pump_count(plant_id); let consecutive_pump_count = board.board_hal.get_esp().consecutive_pump_count(plant_id);
let last_fertilizer_time = board.board_hal.get_esp().last_fertilizer_time(plant_id);
let (a_builds, b_builds) = board.board_hal.get_sensor_build_minutes(); let (a_builds, b_builds) = board.board_hal.get_sensor_build_minutes();
let state = Self { let state = Self {
sensor_a, sensor_a,
@@ -172,6 +175,7 @@ impl PlantState {
}, },
sensor_a_firmware_build_minutes: a_builds[plant_id], sensor_a_firmware_build_minutes: a_builds[plant_id],
sensor_b_firmware_build_minutes: b_builds[plant_id], sensor_b_firmware_build_minutes: b_builds[plant_id],
last_fertilizer_time,
}; };
if state.is_err() { if state.is_err() {
let _ = board.board_hal.fault(plant_id, true).await; let _ = board.board_hal.fault(plant_id, true).await;
@@ -296,6 +300,7 @@ impl PlantState {
}, },
sensor_a_firmware_build_minutes: self.sensor_a_firmware_build_minutes, sensor_a_firmware_build_minutes: self.sensor_a_firmware_build_minutes,
sensor_b_firmware_build_minutes: self.sensor_b_firmware_build_minutes, sensor_b_firmware_build_minutes: self.sensor_b_firmware_build_minutes,
last_fertilizer_time: self.last_fertilizer_time
} }
} }
} }
@@ -328,4 +333,6 @@ pub struct PlantInfo<'a> {
sensor_a_firmware_build_minutes: Option<u32>, sensor_a_firmware_build_minutes: Option<u32>,
/// firmware build timestamp of sensor B (minutes since Unix epoch); None if unknown /// firmware build timestamp of sensor B (minutes since Unix epoch); None if unknown
sensor_b_firmware_build_minutes: Option<u32>, sensor_b_firmware_build_minutes: Option<u32>,
/// last time when fertilizer was applied
last_fertilizer_time: i64,
} }

View File

@@ -129,6 +129,7 @@ export interface PlantConfig {
pump_time_s: number, pump_time_s: number,
pump_cooldown_min: number, pump_cooldown_min: number,
fertilizer_s: number, fertilizer_s: number,
fertilizer_cooldown_min: number,
pump_hour_start: number, pump_hour_start: number,
pump_hour_end: number, pump_hour_end: number,
pump_limit_ml: number, pump_limit_ml: number,

View File

@@ -83,6 +83,11 @@
<input class="plantvalue" id="plant_${plantId}_fertilizer_s" type="number" min="0" max="60" <input class="plantvalue" id="plant_${plantId}_fertilizer_s" type="number" min="0" max="60"
placeholder="0"> placeholder="0">
</div> </div>
<div class="flexcontainer plantPumpEnabledOnly_${plantId}">
<div class="plantkey">Fertilizer Cooldown (m):</div>
<input class="plantvalue" id="plant_${plantId}_fertilizer_cooldown_min" type="number" min="0" max="20160"
placeholder="1440">
</div>
<div class="flexcontainer plantPumpEnabledOnly_${plantId}"> <div class="flexcontainer plantPumpEnabledOnly_${plantId}">
<div class="plantkey">"Pump Hour Start":</div> <div class="plantkey">"Pump Hour Start":</div>
<select class="plantvalue" id="plant_${plantId}_pump_hour_start">10</select> <select class="plantvalue" id="plant_${plantId}_pump_hour_start">10</select>

View File

@@ -80,6 +80,7 @@ export class PlantView {
private readonly pumpTimeS: HTMLInputElement; private readonly pumpTimeS: HTMLInputElement;
private readonly pumpCooldown: HTMLInputElement; private readonly pumpCooldown: HTMLInputElement;
private readonly fertilizerS: HTMLInputElement; private readonly fertilizerS: HTMLInputElement;
private readonly fertilizerCooldownMin: HTMLInputElement;
private readonly pumpHourStart: HTMLSelectElement; private readonly pumpHourStart: HTMLSelectElement;
private readonly pumpHourEnd: HTMLSelectElement; private readonly pumpHourEnd: HTMLSelectElement;
private readonly sensorAInstalled: HTMLInputElement; private readonly sensorAInstalled: HTMLInputElement;
@@ -186,6 +187,11 @@ export class PlantView {
controller.configChanged() controller.configChanged()
} }
this.fertilizerCooldownMin = document.getElementById("plant_" + plantId + "_fertilizer_cooldown_min") as HTMLInputElement;
this.fertilizerCooldownMin.onchange = function () {
controller.configChanged()
}
this.pumpHourStart = document.getElementById("plant_" + plantId + "_pump_hour_start") as HTMLSelectElement; this.pumpHourStart = document.getElementById("plant_" + plantId + "_pump_hour_start") as HTMLSelectElement;
this.pumpHourStart.onchange = function () { this.pumpHourStart.onchange = function () {
controller.configChanged() controller.configChanged()
@@ -335,6 +341,7 @@ export class PlantView {
this.pumpTimeS.value = plantConfig.pump_time_s.toString(); this.pumpTimeS.value = plantConfig.pump_time_s.toString();
this.pumpCooldown.value = plantConfig.pump_cooldown_min.toString(); this.pumpCooldown.value = plantConfig.pump_cooldown_min.toString();
this.fertilizerS.value = plantConfig.fertilizer_s?.toString() || "0"; this.fertilizerS.value = plantConfig.fertilizer_s?.toString() || "0";
this.fertilizerCooldownMin.value = plantConfig.fertilizer_cooldown_min?.toString() || "1440";
this.pumpHourStart.value = plantConfig.pump_hour_start.toString(); this.pumpHourStart.value = plantConfig.pump_hour_start.toString();
this.pumpHourEnd.value = plantConfig.pump_hour_end.toString(); this.pumpHourEnd.value = plantConfig.pump_hour_end.toString();
this.sensorBInstalled.checked = plantConfig.sensor_b; this.sensorBInstalled.checked = plantConfig.sensor_b;
@@ -363,6 +370,7 @@ export class PlantView {
pump_limit_ml: 5000, pump_limit_ml: 5000,
pump_cooldown_min: this.pumpCooldown.valueAsNumber, pump_cooldown_min: this.pumpCooldown.valueAsNumber,
fertilizer_s: this.fertilizerS.valueAsNumber || 0, fertilizer_s: this.fertilizerS.valueAsNumber || 0,
fertilizer_cooldown_min: this.fertilizerCooldownMin.valueAsNumber || 1440,
pump_hour_start: +this.pumpHourStart.value, pump_hour_start: +this.pumpHourStart.value,
pump_hour_end: +this.pumpHourEnd.value, pump_hour_end: +this.pumpHourEnd.value,
sensor_b: this.sensorBInstalled.checked, sensor_b: this.sensorBInstalled.checked,