demeter
Autonomous Hydroponic Intelligencegit clone https://github.com/maydayv7/demeter
agent/tools/actuation.py (4860B)
1from pydantic import BaseModel
2from typing import Dict, Any
3
4# --- USER PROVIDED MODEL ---
5class FarmAction(BaseModel):
6 acid_dosage_ml: float = 0.0
7 base_dosage_ml: float = 0.0
8 nutrient_dosage_ml: float = 0.0
9 fan_speed_pct: float = 0.0
10 water_refill_l: float = 0.0
11
12# --- CALIBRATION CONSTANTS ---
13# How much does 1ml of solution change the reservoir?
14# Assuming a ~50L reservoir for this simulation
15RESERVOIR_LITERS = 50.0
16PH_STRENGTH = 0.02 # 1ml changes 50L by 0.02 pH
17EC_STRENGTH = 0.05 # 1ml changes 50L by 0.05 EC
18
19def convert_targets_to_actions(current_state: Dict[str, float], target_state: Dict[str, float]) -> FarmAction:
20 """
21 Acts as a Proportional Controller.
22 Calculates the exact dosages/fan speeds needed to hit the targets.
23 """
24 action = FarmAction()
25
26 print(f"Current State: {current_state}")
27 print(f"Target State: {target_state}")
28
29 # Extract common values
30 current_temp = current_state.get('air_temp', 25)
31 target_temp = target_state.get('air_temp', 25)
32 current_rh = current_state.get('humidity', 60)
33 target_rh = target_state.get('humidity', 60)
34 current_light = current_state.get('light_intensity', 50)
35 target_light = target_state.get('light_intensity', 50)
36
37 # 1. pH CONTROL (Acid/Base)
38 current_ph = next((v for k, v in current_state.items() if k.lower() == 'ph'), 6.0)
39 target_ph = next((v for k, v in target_state.items() if k.lower() == 'ph'), 6.0)
40 ph_error = target_ph - current_ph
41
42 # Deadband: Don't dose if within 0.1
43 if abs(ph_error) > 0.1:
44 needed_change = abs(ph_error)
45 # Formula: Dose = (Delta / Strength)
46 dose = needed_change / PH_STRENGTH
47
48 if ph_error < 0:
49 # Current is too high -> Need Acid
50 action.acid_dosage_ml = round(dose, 2)
51 else:
52 # Current is too low -> Need Base
53 action.base_dosage_ml = round(dose, 2)
54
55 # 2. EC CONTROL (Nutrients/Water)
56 current_ec = next((v for k, v in current_state.items() if k.lower() == 'ec'), 6.0)
57 target_ec = next((v for k, v in target_state.items() if k.lower() == 'ec'), 6.0)
58 ec_error = target_ec - current_ec
59
60 # Track water needed for EC control
61 water_for_dilution = 0.0
62
63 if abs(ec_error) > 0.1:
64 if ec_error > 0:
65 # Current is too low -> Add Nutrients
66 dose = ec_error / EC_STRENGTH
67 action.nutrient_dosage_ml = round(dose, 2)
68 else:
69 # Current is too high -> Dilute with Water
70 # Rough heuristic: Add 1L water to drop EC by ~0.1
71 water_for_dilution = abs(ec_error) * 10
72
73 # 3. TRANSPIRATION-BASED WATER REPLENISHMENT
74 # Plants lose water continuously through transpiration based on environmental conditions
75 # Base transpiration: 0.5 L/day for a typical hydroponic system
76 # Increase with temperature, decrease with humidity, increase with light
77
78 base_transpiration_per_4h = 0.5 / 6 # ~0.083 L per 4-hour cycle
79
80 # Temperature effect: higher = more transpiration (optimal ~25C, max at 30C+)
81 temp_factor = 1.0
82 if target_temp > 25:
83 temp_factor += (target_temp - 25) * 0.05 # +5% per degree above 25C
84 elif target_temp < 20:
85 temp_factor -= (20 - target_temp) * 0.02 # -2% per degree below 20C
86 temp_factor = max(0.5, min(2.0, temp_factor)) # Clamp between 0.5 and 2.0
87
88 # Humidity effect: lower humidity = more transpiration (inverse)
89 humidity_factor = 1.0
90 if target_rh < 50:
91 humidity_factor += (50 - target_rh) * 0.01 # Drier = more transpiration
92 elif target_rh > 80:
93 humidity_factor -= (target_rh - 80) * 0.01 # Very humid = less transpiration
94 humidity_factor = max(0.5, min(2.0, humidity_factor))
95
96 # Light effect: more light = more photosynthesis = more transpiration
97 light_factor = 1.0 + (target_light / 100.0) * 0.5 # 0% light = 1x, 100% light = 1.5x
98
99 # Calculate total transpiration water needed
100 transpiration_water = base_transpiration_per_4h * temp_factor * humidity_factor * light_factor
101 transpiration_water = round(transpiration_water, 2)
102
103 # 4. TOTAL WATER REFILL = Transpiration + Dilution
104 action.water_refill_l = round(transpiration_water + water_for_dilution, 2)
105
106 # 5. ATMOSPHERIC CONTROL (Fans)
107 # Fans cool down air and lower humidity
108
109 # Simple Logic: If too hot OR too humid, ramp up fans
110 temp_error = current_temp - target_temp
111 rh_error = current_rh - target_rh
112
113 fan_speed = 0.0
114 if temp_error > 0: fan_speed += temp_error * 10 # +1C = +10% speed
115 if rh_error > 0: fan_speed += rh_error * 2 # +1% RH = +2% speed
116
117 # Minimum circulation
118 fan_speed = max(10.0, fan_speed)
119 # Clamp to 100%
120 action.fan_speed_pct = min(100.0, round(fan_speed, 1))
121
122 return action