Compare commits
3 Commits
7fc7661dad
...
ce816af9e7
| Author | SHA1 | Date | |
|---|---|---|---|
| ce816af9e7 | |||
| 519cb25038 | |||
| f81d89980b |
@@ -60,19 +60,19 @@ def send_discord_message(message, username="Auto Garden Bot", is_alert=False, de
|
|||||||
# Quick mem check before importing urequests/SSL
|
# Quick mem check before importing urequests/SSL
|
||||||
mem = getattr(gc, "mem_free", lambda: None)()
|
mem = getattr(gc, "mem_free", lambda: None)()
|
||||||
# Require larger headroom based on device testing (adjust if you re-test)
|
# Require larger headroom based on device testing (adjust if you re-test)
|
||||||
if mem is not None and mem < 150000:
|
if mem is not None and mem < 105000:
|
||||||
# quietly skip send when memory is insufficient
|
print("Discord send skipped: ENOMEM ({} bytes free)".format(mem))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Import urequests only when we plan to send
|
# Import urequests only when we plan to send
|
||||||
try:
|
try:
|
||||||
import urequests as requests # type: ignore
|
import urequests as requests # type: ignore
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
print("Discord send failed: urequests import error:", e)
|
||||||
try:
|
try:
|
||||||
_NEXT_ALLOWED_SEND_TS = time.time() + 60
|
_NEXT_ALLOWED_SEND_TS = time.time() + 60
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
# quiet failure to avoid spamming serial; caller can check return value
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
gc.collect()
|
gc.collect()
|
||||||
@@ -93,14 +93,13 @@ def send_discord_message(message, username="Auto Garden Bot", is_alert=False, de
|
|||||||
return bool(status and 200 <= status < 300)
|
return bool(status and 200 <= status < 300)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# On ENOMEM/MemoryError back off
|
print("Discord send failed:", e)
|
||||||
try:
|
try:
|
||||||
if ("ENOMEM" in str(e)) or isinstance(e, MemoryError):
|
if ("ENOMEM" in str(e)) or isinstance(e, MemoryError):
|
||||||
import time # type: ignore
|
import time # type: ignore
|
||||||
_NEXT_ALLOWED_SEND_TS = time.time() + 60
|
_NEXT_ALLOWED_SEND_TS = time.time() + 60
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
# quiet exception path; return False for caller to handle/backoff
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -58,12 +58,6 @@ class TempWebServer:
|
|||||||
bytes_read = len(body_so_far.encode('utf-8'))
|
bytes_read = len(body_so_far.encode('utf-8'))
|
||||||
bytes_needed = content_length - bytes_read
|
bytes_needed = content_length - bytes_read
|
||||||
|
|
||||||
# ===== DEBUG: Print body reading info =====
|
|
||||||
print("DEBUG POST: Content-Length = {} bytes".format(content_length))
|
|
||||||
print("DEBUG POST: Already read = {} bytes".format(bytes_read))
|
|
||||||
print("DEBUG POST: Still need = {} bytes".format(bytes_needed))
|
|
||||||
# ===== END DEBUG =====
|
|
||||||
|
|
||||||
# Read remaining body in loop (recv() may not return all at once!)
|
# Read remaining body in loop (recv() may not return all at once!)
|
||||||
if bytes_needed > 0:
|
if bytes_needed > 0:
|
||||||
remaining_parts = []
|
remaining_parts = []
|
||||||
@@ -77,21 +71,10 @@ class TempWebServer:
|
|||||||
break
|
break
|
||||||
remaining_parts.append(chunk)
|
remaining_parts.append(chunk)
|
||||||
total_read += len(chunk)
|
total_read += len(chunk)
|
||||||
print("DEBUG POST: Read {} bytes (total: {}/{})".format(
|
|
||||||
len(chunk), total_read, bytes_needed))
|
|
||||||
|
|
||||||
remaining = b''.join(remaining_parts)
|
remaining = b''.join(remaining_parts)
|
||||||
print("DEBUG POST: Read additional {} bytes (expected {})".format(
|
|
||||||
len(remaining), bytes_needed))
|
|
||||||
request = request[:header_end] + body_so_far + remaining.decode('utf-8')
|
request = request[:header_end] + body_so_far + remaining.decode('utf-8')
|
||||||
|
|
||||||
# ===== DEBUG: Print final body length =====
|
|
||||||
final_body = request[header_end:]
|
|
||||||
print("DEBUG POST: Final body length = {} bytes (expected {})".format(
|
|
||||||
len(final_body), content_length))
|
|
||||||
print("DEBUG POST: First 100 chars = {}".format(final_body[:100]))
|
|
||||||
# ===== END DEBUG =====
|
|
||||||
|
|
||||||
if 'POST /update' in request:
|
if 'POST /update' in request:
|
||||||
response = self._handle_update(request, sensors, ac_monitor, heater_monitor, schedule_monitor, config)
|
response = self._handle_update(request, sensors, ac_monitor, heater_monitor, schedule_monitor, config)
|
||||||
# If error page redirects, handle it
|
# If error page redirects, handle it
|
||||||
@@ -118,7 +101,6 @@ class TempWebServer:
|
|||||||
for i in range(0, len(response_bytes), chunk_size):
|
for i in range(0, len(response_bytes), chunk_size):
|
||||||
chunk = response_bytes[i:i+chunk_size]
|
chunk = response_bytes[i:i+chunk_size]
|
||||||
conn.sendall(chunk)
|
conn.sendall(chunk)
|
||||||
print("DEBUG: Sent chunk {} ({} bytes)".format(i//chunk_size + 1, len(chunk)))
|
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
print("DEBUG: Schedule editor page sent successfully ({} bytes total)".format(len(response_bytes)))
|
print("DEBUG: Schedule editor page sent successfully ({} bytes total)".format(len(response_bytes)))
|
||||||
@@ -1400,8 +1382,6 @@ document.addEventListener('DOMContentLoaded', function() {{
|
|||||||
# Build schedule inputs
|
# Build schedule inputs
|
||||||
schedule_inputs = ""
|
schedule_inputs = ""
|
||||||
for i, schedule in enumerate(schedules[:4]):
|
for i, schedule in enumerate(schedules[:4]):
|
||||||
print("DEBUG: Building HTML for schedule {}...".format(i))
|
|
||||||
|
|
||||||
time_value = schedule.get('time', '')
|
time_value = schedule.get('time', '')
|
||||||
name_value = schedule.get('name', '')
|
name_value = schedule.get('name', '')
|
||||||
heater_value = schedule.get('heater_target', config.get('heater_target'))
|
heater_value = schedule.get('heater_target', config.get('heater_target'))
|
||||||
|
|||||||
206
main.py
206
main.py
@@ -22,14 +22,6 @@ except Exception as e:
|
|||||||
# Import after WiFi reset
|
# Import after WiFi reset
|
||||||
from scripts.networking import connect_wifi
|
from scripts.networking import connect_wifi
|
||||||
|
|
||||||
from scripts.monitors import TemperatureMonitor, WiFiMonitor, ACMonitor, HeaterMonitor, run_monitors
|
|
||||||
from scripts.temperature_sensor import TemperatureSensor
|
|
||||||
from scripts.air_conditioning import ACController
|
|
||||||
from scripts.heating import HeaterController
|
|
||||||
from scripts.web_server import TempWebServer
|
|
||||||
from scripts.scheduler import ScheduleMonitor # NEW: Import scheduler for time-based temp changes
|
|
||||||
from scripts.memory_check import check_memory_once # Just the function
|
|
||||||
|
|
||||||
# ===== NEW: NTP Sync Function (imports locally) =====
|
# ===== NEW: NTP Sync Function (imports locally) =====
|
||||||
def sync_ntp_time(timezone_offset):
|
def sync_ntp_time(timezone_offset):
|
||||||
"""
|
"""
|
||||||
@@ -209,7 +201,7 @@ if wifi and wifi.isconnected():
|
|||||||
gc.collect()
|
gc.collect()
|
||||||
ram_free = gc.mem_free()
|
ram_free = gc.mem_free()
|
||||||
print(f"DEBUG: Free RAM before Discord send: {ram_free // 1024} KB")
|
print(f"DEBUG: Free RAM before Discord send: {ram_free // 1024} KB")
|
||||||
mem_ok = ram_free > 100000
|
mem_ok = ram_free > 105000
|
||||||
if mem_ok:
|
if mem_ok:
|
||||||
ok = discord_webhook.send_discord_message("Pico W online at http://{}".format(ifconfig[0]), debug=False)
|
ok = discord_webhook.send_discord_message("Pico W online at http://{}".format(ifconfig[0]), debug=False)
|
||||||
if ok:
|
if ok:
|
||||||
@@ -224,6 +216,15 @@ if wifi and wifi.isconnected():
|
|||||||
pending_discord_message = "Pico W online at http://{}".format(ifconfig[0])
|
pending_discord_message = "Pico W online at http://{}".format(ifconfig[0])
|
||||||
discord_send_attempts = 1
|
discord_send_attempts = 1
|
||||||
|
|
||||||
|
# ===== Moved to later so discord could fire off startup message hopefully =====
|
||||||
|
from scripts.monitors import TemperatureMonitor, WiFiMonitor, ACMonitor, HeaterMonitor, run_monitors
|
||||||
|
from scripts.temperature_sensor import TemperatureSensor
|
||||||
|
from scripts.air_conditioning import ACController
|
||||||
|
from scripts.heating import HeaterController
|
||||||
|
from scripts.web_server import TempWebServer
|
||||||
|
from scripts.scheduler import ScheduleMonitor
|
||||||
|
from scripts.memory_check import check_memory_once
|
||||||
|
|
||||||
# Start web server early so page can load even if time sync is slow
|
# Start web server early so page can load even if time sync is slow
|
||||||
web_server = TempWebServer(port=80)
|
web_server = TempWebServer(port=80)
|
||||||
web_server.start()
|
web_server.start()
|
||||||
@@ -360,47 +361,6 @@ print("="*50 + "\n")
|
|||||||
check_memory_once()
|
check_memory_once()
|
||||||
# ===== END: Startup Memory Check =====
|
# ===== END: Startup Memory Check =====
|
||||||
|
|
||||||
# ===== START: Monitor Setup =====
|
|
||||||
# Set up all monitoring systems (run in order during main loop)
|
|
||||||
monitors = [
|
|
||||||
# WiFi monitor: Checks connection, reconnects if needed, blinks LED
|
|
||||||
WiFiMonitor(wifi, led, interval=5, reconnect_cooldown=60, config=config),
|
|
||||||
|
|
||||||
# Schedule monitor: Changes temp targets based on time of day
|
|
||||||
schedule_monitor,
|
|
||||||
|
|
||||||
# AC monitor: Automatically turns AC on/off based on temperature
|
|
||||||
ac_monitor,
|
|
||||||
|
|
||||||
# Heater monitor: Automatically turns heater on/off based on temperature
|
|
||||||
heater_monitor,
|
|
||||||
|
|
||||||
# Inside temperature monitor: Logs temps, sends alerts if out of range
|
|
||||||
TemperatureMonitor(
|
|
||||||
sensor=sensors['inside'],
|
|
||||||
label=SENSOR_CONFIG['inside']['label'],
|
|
||||||
check_interval=10, # Check temp every 10 seconds
|
|
||||||
report_interval=30, # Log to CSV every 30 seconds
|
|
||||||
alert_high=SENSOR_CONFIG['inside']['alert_high'], # High temp alert threshold
|
|
||||||
alert_low=SENSOR_CONFIG['inside']['alert_low'], # Low temp alert threshold
|
|
||||||
log_file="/temp_logs.csv", # CSV file path
|
|
||||||
send_alerts_to_separate_channel=True # Use separate Discord channel
|
|
||||||
),
|
|
||||||
|
|
||||||
# Outside temperature monitor: Logs temps, sends alerts if out of range
|
|
||||||
TemperatureMonitor(
|
|
||||||
sensor=sensors['outside'],
|
|
||||||
label=SENSOR_CONFIG['outside']['label'],
|
|
||||||
check_interval=10, # Check temp every 10 seconds
|
|
||||||
report_interval=30, # Log to CSV every 30 seconds
|
|
||||||
alert_high=SENSOR_CONFIG['outside']['alert_high'], # High temp alert threshold
|
|
||||||
alert_low=SENSOR_CONFIG['outside']['alert_low'], # Low temp alert threshold
|
|
||||||
log_file="/temp_logs.csv", # CSV file path
|
|
||||||
send_alerts_to_separate_channel=False # Use main Discord channel
|
|
||||||
),
|
|
||||||
]
|
|
||||||
# ===== END: Monitor Setup =====
|
|
||||||
|
|
||||||
print("Starting monitoring loop...")
|
print("Starting monitoring loop...")
|
||||||
print("Press Ctrl+C to stop\n")
|
print("Press Ctrl+C to stop\n")
|
||||||
|
|
||||||
@@ -411,75 +371,93 @@ last_ntp_sync = time.time() # Track when we last synced
|
|||||||
|
|
||||||
# ===== START: Main Loop =====
|
# ===== START: Main Loop =====
|
||||||
# Main monitoring loop (runs forever until Ctrl+C)
|
# Main monitoring loop (runs forever until Ctrl+C)
|
||||||
|
last_monitor_run = {
|
||||||
|
"wifi": 0,
|
||||||
|
"schedule": 0,
|
||||||
|
"ac": 0,
|
||||||
|
"heater": 0,
|
||||||
|
"inside_temp": 0,
|
||||||
|
"outside_temp": 0,
|
||||||
|
}
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
now = time.time()
|
||||||
# Try to send pending discord startup message when memory permits
|
|
||||||
if not discord_sent and pending_discord_message and discord_send_attempts < 3:
|
|
||||||
import gc as _gc # type: ignore
|
|
||||||
_gc.collect()
|
|
||||||
_gc.collect()
|
|
||||||
mem_ok = getattr(_gc, 'mem_free', lambda: 0)() > 100000
|
|
||||||
if mem_ok:
|
|
||||||
try:
|
|
||||||
ok = discord_webhook.send_discord_message(pending_discord_message, debug=False)
|
|
||||||
if ok:
|
|
||||||
print("Discord startup notification sent")
|
|
||||||
discord_sent = True
|
|
||||||
else:
|
|
||||||
discord_send_attempts += 1
|
|
||||||
if discord_send_attempts >= 3:
|
|
||||||
print("Discord startup notification failed after retries")
|
|
||||||
discord_sent = True
|
|
||||||
except Exception:
|
|
||||||
discord_send_attempts += 1
|
|
||||||
if discord_send_attempts >= 3:
|
|
||||||
discord_sent = True
|
|
||||||
|
|
||||||
# Run all monitors (each checks if it's time to run via should_run())
|
# WiFi monitor every 5 seconds (can be stateless)
|
||||||
run_monitors(monitors)
|
if now - last_monitor_run["wifi"] >= 5:
|
||||||
|
from scripts.monitors import WiFiMonitor
|
||||||
|
wifi_monitor = WiFiMonitor(wifi, led, interval=5, reconnect_cooldown=60, config=config)
|
||||||
|
try:
|
||||||
|
wifi_monitor.run()
|
||||||
|
except Exception as e:
|
||||||
|
print("WiFiMonitor error:", e)
|
||||||
|
del wifi_monitor
|
||||||
|
gc.collect()
|
||||||
|
last_monitor_run["wifi"] = now
|
||||||
|
|
||||||
# Web requests
|
# Schedule monitor every 60 seconds (persistent)
|
||||||
web_server.check_requests(sensors, ac_monitor, heater_monitor, schedule_monitor, config)
|
if now - last_monitor_run["schedule"] >= 60:
|
||||||
|
try:
|
||||||
|
schedule_monitor.run()
|
||||||
|
except Exception as e:
|
||||||
|
print("ScheduleMonitor error:", e)
|
||||||
|
last_monitor_run["schedule"] = now
|
||||||
|
|
||||||
# ===== PERIODIC RE-SYNC (every 24 hours) =====
|
# AC monitor every 30 seconds (persistent)
|
||||||
if ntp_synced and (time.time() - last_ntp_sync) > 86400:
|
if now - last_monitor_run["ac"] >= 30:
|
||||||
print("24-hour re-sync due...")
|
try:
|
||||||
if sync_ntp_time(TIMEZONE_OFFSET):
|
ac_monitor.run()
|
||||||
last_ntp_sync = time.time()
|
except Exception as e:
|
||||||
print("Daily NTP re-sync successful")
|
print("ACMonitor error:", e)
|
||||||
else:
|
last_monitor_run["ac"] = now
|
||||||
print("Daily NTP re-sync failed (will retry tomorrow)")
|
|
||||||
# ===== END: PERIODIC RE-SYNC =====
|
|
||||||
|
|
||||||
# Aggressive GC without frequent console noise
|
# Heater monitor every 30 seconds (persistent)
|
||||||
current_time = time.time()
|
if now - last_monitor_run["heater"] >= 30:
|
||||||
if int(current_time) % 5 == 0:
|
try:
|
||||||
gc.collect()
|
heater_monitor.run()
|
||||||
# Print memory stats infrequently (every 10 minutes)
|
except Exception as e:
|
||||||
if int(current_time) % 600 == 0:
|
print("HeaterMonitor error:", e)
|
||||||
print("Memory free: {} KB".format(gc.mem_free() // 1024))
|
last_monitor_run["heater"] = now
|
||||||
# ===== END: AGGRESSIVE GC =====
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
# Inside temperature monitor every 10 seconds (can be stateless)
|
||||||
# Graceful shutdown on Ctrl+C
|
if now - last_monitor_run["inside_temp"] >= 10:
|
||||||
print("\n\n" + "="*50)
|
from scripts.monitors import TemperatureMonitor
|
||||||
print("Shutting down gracefully...")
|
inside_monitor = TemperatureMonitor(
|
||||||
print("="*50)
|
sensor=sensors['inside'],
|
||||||
print("Turning off AC...")
|
label=SENSOR_CONFIG['inside']['label'],
|
||||||
ac_controller.turn_off()
|
check_interval=10,
|
||||||
print("Turning off heater...")
|
report_interval=30,
|
||||||
heater_controller.turn_off()
|
alert_high=SENSOR_CONFIG['inside']['alert_high'],
|
||||||
print("Turning off LED...")
|
alert_low=SENSOR_CONFIG['inside']['alert_low'],
|
||||||
led.low()
|
log_file="/temp_logs.csv",
|
||||||
print("Shutdown complete!")
|
send_alerts_to_separate_channel=True
|
||||||
print("="*50 + "\n")
|
)
|
||||||
break
|
inside_monitor.run()
|
||||||
|
del inside_monitor
|
||||||
|
gc.collect()
|
||||||
|
last_monitor_run["inside_temp"] = now
|
||||||
|
|
||||||
except Exception as e:
|
# Outside temperature monitor every 10 seconds (can be stateless)
|
||||||
# If loop crashes, print error and keep running
|
if now - last_monitor_run["outside_temp"] >= 10:
|
||||||
print("❌ Main loop error: {}".format(e))
|
from scripts.monitors import TemperatureMonitor
|
||||||
import sys
|
outside_monitor = TemperatureMonitor(
|
||||||
sys.print_exception(e)
|
sensor=sensors['outside'],
|
||||||
time.sleep(5) # Brief pause before retrying
|
label=SENSOR_CONFIG['outside']['label'],
|
||||||
|
check_interval=10,
|
||||||
|
report_interval=30,
|
||||||
|
alert_high=SENSOR_CONFIG['outside']['alert_high'],
|
||||||
|
alert_low=SENSOR_CONFIG['outside']['alert_low'],
|
||||||
|
log_file="/temp_logs.csv",
|
||||||
|
send_alerts_to_separate_channel=False
|
||||||
|
)
|
||||||
|
outside_monitor.run()
|
||||||
|
del outside_monitor
|
||||||
|
gc.collect()
|
||||||
|
last_monitor_run["outside_temp"] = now
|
||||||
|
|
||||||
|
# Web requests (keep web server loaded if needed)
|
||||||
|
web_server.check_requests(sensors, ac_monitor, heater_monitor, schedule_monitor, config)
|
||||||
|
|
||||||
|
gc.collect()
|
||||||
|
time.sleep(0.1)
|
||||||
# ===== END: Main Loop =====
|
# ===== END: Main Loop =====
|
||||||
Reference in New Issue
Block a user