Compare commits

...

3 Commits

3 changed files with 100 additions and 143 deletions

View File

@@ -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:

View File

@@ -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,20 +71,9 @@ 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)
@@ -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'))

212
main.py
View File

@@ -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 ===== # Heater monitor every 30 seconds (persistent)
if now - last_monitor_run["heater"] >= 30:
# Aggressive GC without frequent console noise try:
current_time = time.time() heater_monitor.run()
if int(current_time) % 5 == 0: except Exception as e:
gc.collect() print("HeaterMonitor error:", e)
# Print memory stats infrequently (every 10 minutes) last_monitor_run["heater"] = now
if int(current_time) % 600 == 0:
print("Memory free: {} KB".format(gc.mem_free() // 1024)) # Inside temperature monitor every 10 seconds (can be stateless)
# ===== END: AGGRESSIVE GC ===== if now - last_monitor_run["inside_temp"] >= 10:
time.sleep(0.1) from scripts.monitors import TemperatureMonitor
inside_monitor = TemperatureMonitor(
except KeyboardInterrupt: sensor=sensors['inside'],
# Graceful shutdown on Ctrl+C label=SENSOR_CONFIG['inside']['label'],
print("\n\n" + "="*50) check_interval=10,
print("Shutting down gracefully...") report_interval=30,
print("="*50) alert_high=SENSOR_CONFIG['inside']['alert_high'],
print("Turning off AC...") alert_low=SENSOR_CONFIG['inside']['alert_low'],
ac_controller.turn_off() log_file="/temp_logs.csv",
print("Turning off heater...") send_alerts_to_separate_channel=True
heater_controller.turn_off() )
print("Turning off LED...") inside_monitor.run()
led.low() del inside_monitor
print("Shutdown complete!") gc.collect()
print("="*50 + "\n") last_monitor_run["inside_temp"] = now
break
# Outside temperature monitor every 10 seconds (can be stateless)
except Exception as e: if now - last_monitor_run["outside_temp"] >= 10:
# If loop crashes, print error and keep running from scripts.monitors import TemperatureMonitor
print("❌ Main loop error: {}".format(e)) outside_monitor = TemperatureMonitor(
import sys sensor=sensors['outside'],
sys.print_exception(e) label=SENSOR_CONFIG['outside']['label'],
time.sleep(5) # Brief pause before retrying 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 =====