diff --git a/Scripts/Common.py b/Scripts/Common.py index 3b7f6f0..e59bca9 100644 --- a/Scripts/Common.py +++ b/Scripts/Common.py @@ -22,10 +22,21 @@ CONFIG_UPDATE_AVAILABLE = True NO_CONFIG_UPDATE = False -TANK_HEIGHT = 60 # Example tank height -TOP_EMPTY_DISTANCE = 5 -BOTTOM_FULL_DISTANCE = 5 -ONE_THIRD_LEVEL = TANK_HEIGHT / 3 +TANK_HEIGHT = 125 # Example tank height +TOP_EMPTY_DISTANCE = 25 +BOTTOM_FULL_DISTANCE = 65 +ONE_THIRD_LEVEL = TANK_HEIGHT // 3 CHECK_INTERVAL_SECONDS = 1 MAX_PRE_NIGHT_FILL_TIME = 600 + +MORNING_7AM = 7 +MORNING_8AM = 8 +EVENING_7PM = 19 +EVENING_8PM = 20 +NIGHT_10PM = 22 +NIGHT_9PM = 21 + + +VALVE1_DEFAULT_DURATION = 1 # in minutes +VALVE2_DEFAULT_DURATION = 1 # in minutes diff --git a/Scripts/Main.py b/Scripts/Main.py index 3cda63f..49a0f72 100644 --- a/Scripts/Main.py +++ b/Scripts/Main.py @@ -11,11 +11,18 @@ def setupGpio(gpio:GpioManager): gpio.setup(MOTOR_PIN, True) gpio.setup(ULTRASONIC_TRIG, True) gpio.setup(ULTRASONIC_ECHO, False) + gpio.setup(VALVE1_PIN, True) + gpio.setup(VALVE2_PIN, True) + gpio.output(MOTOR_PIN, False) + gpio.output(VALVE1_PIN, False) + gpio.output(VALVE2_PIN, False) def cleanupGpio(gpio:GpioManager): gpio.output(MOTOR_PIN, False) + gpio.output(VALVE1_PIN, False) + gpio.output(VALVE2_PIN, False) gpio.cleanup() @@ -51,25 +58,90 @@ def readDistance(gpio:GpioManager, trig, echo): def isNightTime(): now = datetime.now() hour = now.hour - return hour >= 22 or hour < 7 + return hour >= NIGHT_10PM or hour < MORNING_7AM def main(): gpio = GpioManager() setupGpio(gpio) + + # Default valves and durations + valve1Duration = VALVE1_DEFAULT_DURATION + valve2Duration = VALVE2_DEFAULT_DURATION + valve1On = False + valve2On = False + valve1StartTime = 0 + valve2StartTime = 0 + morningRunDone = False + eveningRunDone = False + motorStatus = "OFF" + lastMotorStatus = "OFF" + waterLevel = 0 + lastWaterLevel = 0 + lastDay = datetime.now().day + try: lastCheckTime = time.time() preNightFillActive = False preNightFillStart = None while True: currentTime = time.time() + now = datetime.now() + + # Reset daily flags + if now.day != lastDay: + morningRunDone = False + eveningRunDone = False + lastDay = now.day + + rtDb = readRtDb() + configUpdateAvailable = rtDb.get("configUpdateAvailable", False) + + # Check for config updates for valves + if configUpdateAvailable: + valve1Duration = rtDb.get("valve1Duration", VALVE1_DEFAULT_DURATION) + valve2Duration = rtDb.get("valve2Duration", VALVE2_DEFAULT_DURATION) + print(f"Updated valve durations: Valve1={valve1Duration} min, Valve2={valve2Duration} min") + + # Morning valve operation + if now.hour == MORNING_8AM and not morningRunDone: + print("Morning run: Activating valves.") + gpio.output(VALVE1_PIN, True) + gpio.output(VALVE2_PIN, True) + valve1On = True + valve2On = True + valve1StartTime = currentTime + valve2StartTime = currentTime + morningRunDone = True + + # Evening valve operation + if now.hour == EVENING_8PM and not eveningRunDone: + print("Evening run: Activating valves.") + gpio.output(VALVE1_PIN, True) + gpio.output(VALVE2_PIN, True) + valve1On = True + valve2On = True + valve1StartTime = currentTime + valve2StartTime = currentTime + eveningRunDone = True + + # Check to turn off valves + if valve1On and (currentTime - valve1StartTime >= valve1Duration * 60): + gpio.output(VALVE1_PIN, False) + valve1On = False + print("Valve 1 duration complete. Turned off.") + + if valve2On and (currentTime - valve2StartTime >= valve2Duration * 60): + gpio.output(VALVE2_PIN, False) + valve2On = False + print("Valve 2 duration complete. Turned off.") + + # Original motor logic if currentTime - lastCheckTime >= CHECK_INTERVAL_SECONDS: distance = readDistance(gpio, ULTRASONIC_TRIG, ULTRASONIC_ECHO) print(f"Distance: {distance} cm") waterLevel = TANK_HEIGHT - distance - now = datetime.now() rtDb = readRtDb() - configUpdateAvailable = rtDb.get("configUpdateAvailable", False) motorStatus = rtDb.get("motorStatus", "OFF") if configUpdateAvailable: @@ -80,18 +152,16 @@ def main(): else: gpio.output(MOTOR_PIN, False) print("Config update: Motor OFF") - # Reset configUpdateAvailable after applying - writeRtDb(motorStatus=motorStatus, tankLevel=waterLevel, configUpdateAvailable=False) else: # Automatic logic motorStatus = "OFF" - if isNightTime(): + if isNightTime(): # Check if it's night time between 10 PM and 7 AM gpio.output(MOTOR_PIN, False) motorStatus = "OFF" print("Night time: Motor OFF") preNightFillActive = False else: - if now.hour == 21 and waterLevel < ONE_THIRD_LEVEL and not preNightFillActive: + if now.hour == NIGHT_9PM and waterLevel < ONE_THIRD_LEVEL and not preNightFillActive: print("Pre-night: Water < 1/3, filling tank...") gpio.output(MOTOR_PIN, True) motorStatus = "ON" @@ -119,10 +189,15 @@ def main(): gpio.output(MOTOR_PIN, False) motorStatus = "OFF" print("Tank level OK: Motor OFF") - writeRtDb(motorStatus=motorStatus, tankLevel=waterLevel, configUpdateAvailable=False) + lastCheckTime = currentTime - time.sleep(0.1) - # Short sleep to reduce CPU usage and maintain responsiveness + + if configUpdateAvailable or motorStatus != lastMotorStatus or waterLevel != lastWaterLevel: + writeRtDb(motorStatus = motorStatus, tankLevel = waterLevel, configUpdateAvailable = False) + lastMotorStatus = motorStatus + lastWaterLevel = waterLevel + + time.sleep(0.1) # Sleep to reduce CPU usage except KeyboardInterrupt: cleanupGpio(gpio) diff --git a/Scripts/PinDescription.py b/Scripts/PinDescription.py index 7cba24a..ea92361 100644 --- a/Scripts/PinDescription.py +++ b/Scripts/PinDescription.py @@ -1,3 +1,6 @@ MOTOR_PIN = 32 ULTRASONIC_TRIG = 40 ULTRASONIC_ECHO = 38 + +VALVE1_PIN = 36 +VALVE2_PIN = 37 diff --git a/Web/index.html b/Web/index.html index f38b976..288ddb5 100644 --- a/Web/index.html +++ b/Web/index.html @@ -16,6 +16,20 @@

Motor Status

+ +
+

Valve Configuration

+
+ + +

Selected:

+
+
+ + +

Selected:

+
+
diff --git a/Web/motor.php b/Web/motor.php index 89cdd2b..fdfd142 100644 --- a/Web/motor.php +++ b/Web/motor.php @@ -8,8 +8,10 @@ function getStatus() { return ["error" => "rtDb.json not found"]; } return [ - "motorStatus" => isset($data['motorStatus']) ? $data['motorStatus'] : 'OFF', - "tankLevel" => isset($data['tankLevel']) ? $data['tankLevel'] : 0, + "motorStatus" => isset($data['motorStatus']) ? $data['motorStatus'] : 'OFF', + "tankLevel" => isset($data['tankLevel']) ? $data['tankLevel'] : 0, + "valve1Duration" => isset($data['valve1Duration']) ? $data['valve1Duration'] : 1, + "valve2Duration" => isset($data['valve2Duration']) ? $data['valve2Duration'] : 1, "configUpdateAvailable" => isset($data['configUpdateAvailable']) ? $data['configUpdateAvailable'] : false ]; } @@ -25,9 +27,26 @@ function setMotor($motorStatus) { return ["success" => true, "motorStatus" => $motorStatus]; } +function setConfig($key, $value) { + $data = readDb(); + if ($data === false) { + return ["error" => "rtDb.json not found"]; + } + $data[$key] = $value; + $data['configUpdateAvailable'] = true; + writeDb($data); + return ["success" => true, $key => $value]; +} + if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $motorStatus = isset($_POST['motorStatus']) && $_POST['motorStatus'] === 'ON' ? 'ON' : 'OFF'; - echo json_encode(setMotor($motorStatus)); + if (isset($_POST['motorStatus'])) { + $motorStatus = $_POST['motorStatus'] === 'ON' ? 'ON' : 'OFF'; + echo json_encode(setMotor($motorStatus)); + } elseif (isset($_POST['valve1Duration'])) { + echo json_encode(setConfig('valve1Duration', (int)$_POST['valve1Duration'])); + } elseif (isset($_POST['valve2Duration'])) { + echo json_encode(setConfig('valve2Duration', (int)$_POST['valve2Duration'])); + } } else { echo json_encode(getStatus()); } diff --git a/Web/script.js b/Web/script.js index a9fa6b7..1e9d526 100644 --- a/Web/script.js +++ b/Web/script.js @@ -15,6 +15,10 @@ function fetchStatus() { toggleBtn.classList.remove('on'); } toggleBtn.disabled = false; + + // Update dropdowns and selected values + updateConfigValue('valve1Duration', data.valve1Duration); + updateConfigValue('valve2Duration', data.valve2Duration); }) .catch(err => { document.getElementById('tankLevel').textContent = 'Error'; @@ -40,6 +44,40 @@ function setMotor(status) { }); } +function setConfig(key, value) { + fetch('motor.php', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `${key}=${value}` + }) + .then(res => res.json()) + .then(() => fetchStatus()) + .catch(err => { + alert('Failed to set config. Network error.'); + fetchStatus(); + console.error('Set config error:', err); + }); +} + +function populateDropdown(selectId) { + const select = document.getElementById(selectId); + for (let i = 1; i <= 15; i++) { + const option = document.createElement('option'); + option.value = i; + option.textContent = `${i} min`; + select.appendChild(option); + } +} + +function updateConfigValue(id, value) { + const select = document.getElementById(id); + const selectedSpan = document.getElementById(`selected${id.charAt(0).toUpperCase() + id.slice(1)}`); + if (value) { + select.value = value; + selectedSpan.textContent = `${value} min`; + } +} + const motorToggleBtn = document.getElementById('motorToggleBtn'); motorToggleBtn.onclick = function() { motorToggleBtn.disabled = true; @@ -50,5 +88,18 @@ motorToggleBtn.onclick = function() { } }; +// Populate dropdowns +populateDropdown('valve1Duration'); +populateDropdown('valve2Duration'); + +// Add event listeners for dropdowns +document.getElementById('valve1Duration').addEventListener('change', function() { + setConfig('valve1Duration', this.value); +}); + +document.getElementById('valve2Duration').addEventListener('change', function() { + setConfig('valve2Duration', this.value); +}); + fetchStatus(); setInterval(fetchStatus, 3000); diff --git a/config.yaml b/config.yaml index 4060a36..b3a5aa0 100644 --- a/config.yaml +++ b/config.yaml @@ -1,2 +1,2 @@ -appVersion: 1.1.0.1002 +appVersion: 1.2.0.1003