Compare commits

..

25 Commits

Author SHA1 Message Date
e90e56c3f8 fix: Update configuration instructions to reflect changes in secrets management and file structure 2025-11-28 10:20:49 -05:00
5cfa36e558 fix: Remove test_send.py script to clean up unused code 2025-11-17 17:16:30 -05:00
c8102e62ee fix: Refactor main loop for graceful shutdown and improved error handling 2025-11-15 14:11:20 -05:00
d76b11430c fix: Remove redundant garbage collection calls in send_discord_message function 2025-11-15 13:54:08 -05:00
cb274545a3 fix: Remove unused variable 'schedules' and optimize garbage collection in schedule handling 2025-11-15 13:53:09 -05:00
6cd1349633 fix: Remove unused variables and trigger garbage collection in schedule handling 2025-11-15 13:49:20 -05:00
bcecf2a81a fix: Add garbage collection calls to optimize memory usage in web server operations 2025-11-15 13:40:41 -05:00
621a48f011 fix: Adjust memory threshold for Discord message sending to enhance reliability 2025-11-15 13:15:45 -05:00
ce816af9e7 fix: Adjust memory threshold for Discord message sending and improve error logging 2025-11-15 13:10:48 -05:00
519cb25038 refactor: Optimize import statements and restructure monitoring logic for improved performance 2025-11-15 13:10:41 -05:00
f81d89980b refactor: Remove debug print statements from POST request handling in TempWebServer 2025-11-15 13:10:32 -05:00
7fc7661dad fix: Adjust memory threshold for Discord message sending and add debug logging for RAM usage 2025-11-15 12:02:50 -05:00
3b7982a3a3 fix: Adjust memory threshold for Discord message sending to improve reliability 2025-11-15 11:58:32 -05:00
697f0bf31e fix: Improve Discord message sending logic and memory management 2025-11-15 11:52:54 -05:00
b632a76d5a refactor: Remove debug_force_send function to streamline message sending process 2025-11-15 11:22:36 -05:00
d670067b89 fix: Optimize memory management in debug_force_send and send_discord_message functions 2025-11-15 10:32:04 -05:00
ac860207d9 fix: Increase memory thresholds for Discord message sending and adjust garbage collection logging 2025-11-15 10:27:26 -05:00
03b26b5339 feat: Add debug_force_send function for memory tracking and testing 2025-11-15 10:19:49 -05:00
5a8d14eb4d fix: Enable debug logging in send_discord_message for better memory tracking 2025-11-15 10:03:15 -05:00
79445bf879 fix: Add debug logging to send_discord_message for memory checks and import impact 2025-11-15 09:58:09 -05:00
4400fb5a74 fix: Adjust memory thresholds for Discord message sending to match device capabilities 2025-11-15 09:46:22 -05:00
c6f46e097b fix: Increase memory thresholds and backoff duration for Discord message sending 2025-11-15 09:42:16 -05:00
d2c0f68488 fix: Enhance Discord message sending with memory checks and scheduling 2025-11-15 09:36:44 -05:00
13e3a56fa6 fix: Add low-memory guard and cooldown for Discord message sending
This isn't quite the fix though just want to save my position till tomorrow and see what changes come up before and after
2025-11-14 21:48:19 -05:00
efea4a1384 fix: Enhance Discord message sending with aggressive GC and low-memory guard 2025-11-14 21:28:10 -05:00
4 changed files with 254 additions and 200 deletions

View File

@@ -146,56 +146,28 @@ RUN pin → Button → GND
### 4. Configuration
**Create `secrets.py`** (copy from `secrets.example.py`):
**Edit `config.json`** (created automatically on first boot, or edit manually):
```python
secrets = {
'ssid': 'YOUR_WIFI_NAME',
'password': 'YOUR_WIFI_PASSWORD',
'discord_webhook_url': 'https://discord.com/api/webhooks/...',
'discord_alert_webhook_url': 'https://discord.com/api/webhooks/...',
}
```
**Sensor Configuration in `main.py`:**
```python
# Sensor configuration
SENSOR_CONFIG = {
'inside': {
'pin': 10,
'label': 'Inside',
'alert_high': 80.0,
'alert_low': 70.0
},
'outside': {
'pin': 11,
'label': 'Outside',
'alert_high': 85.0,
'alert_low': 68.0
}
}
```
**Default Climate Settings (auto-saved to config.json):**
```python
# Default config (created on first boot)
```json
{
"ac_target": 77.0, # AC target temperature (°F)
"ac_swing": 1.0, # AC turns on at 78°F, off at 76°F
"heater_target": 72.0, # Heater target temperature (°F)
"heater_swing": 2.0, # Heater turns on at 70°F, off at 74°F
"temp_hold_duration": 3600, # Temporary hold lasts 1 hour (3600 seconds)
"schedule_enabled": true, # Schedules active by default
"schedules": [ # 4 time-based schedules
"ssid": "YOUR_WIFI_NAME",
"password": "YOUR_WIFI_PASSWORD",
"discord_webhook_url": "https://discord.com/api/webhooks/...",
"discord_alert_webhook_url": "https://discord.com/api/webhooks/...",
"ac_target": 77.0,
"ac_swing": 1.0,
"heater_target": 72.0,
"heater_swing": 2.0,
"temp_hold_duration": 3600,
"schedule_enabled": true,
"schedules": [
{
"time": "06:00",
"name": "Morning",
"ac_target": 75.0,
"heater_target": 72.0
},
# ... 3 more schedules
}
// ... 3 more schedules
]
}
```
@@ -209,7 +181,6 @@ Upload all files to your Pico:
```text
/
├── main.py
├── secrets.py
├── config.json # Auto-generated on first boot
└── scripts/
├── air_conditioning.py # AC/Heater controller classes
@@ -228,9 +199,7 @@ The Pico will auto-start `main.py` on boot and be accessible at **<http://192.16
```text
Auto-Garden/
├── main.py # Entry point, configuration, system initialization
├── secrets.py # WiFi & Discord credentials (gitignored)
├── secrets.example.py # Template for secrets.py
├── config.json # Persistent configuration (auto-generated)
├── config.json # Persistent configuration and credentials (auto-generated)
└── scripts/
├── air_conditioning.py # AC & Heater controllers with short-cycle protection
├── discord_webhook.py # Discord notification handling
@@ -447,14 +416,14 @@ report_interval=30 # Discord report frequency
**WiFi not connecting:**
- Verify SSID/password in `secrets.py`
- Verify SSID/password in `config.json`
- Check 2.4GHz WiFi (Pico W doesn't support 5GHz)
- LED should be solid when connected
- Check serial console for connection status
**Discord messages not sending:**
- Verify webhook URLs in `secrets.py`
- Verify webhook URLs in `config.json`
- Test webhooks with curl/Postman first
- Check Pico has internet access (ping test)
- Look for error messages in serial console

View File

@@ -1,5 +1,7 @@
# Minimal module-level state (only what we need)
_CONFIG = {"discord_webhook_url": None, "discord_alert_webhook_url": None}
# Cooldown after low-memory failures (epoch seconds)
_NEXT_ALLOWED_SEND_TS = 0
def set_config(cfg: dict):
"""Initialize module with minimal values from loaded config (call from main)."""
@@ -25,39 +27,79 @@ def _escape_json_str(s: str) -> str:
s = s.replace("\t", "\\t")
return s
def send_discord_message(message, username="Auto Garden Bot", is_alert=False):
def send_discord_message(message, username="Auto Garden Bot", is_alert=False, debug: bool = False):
"""
Send Discord message. Import urequests locally to avoid occupying RAM when idle.
Send Discord message with aggressive GC and low-memory guard to avoid ENOMEM.
When debug=True prints mem_free at important steps so you can see peak usage.
Returns True on success, False otherwise.
"""
global _NEXT_ALLOWED_SEND_TS
resp = None
url = _get_webhook_url(is_alert=is_alert)
if not url:
if debug: print("DBG: no webhook URL configured")
return False
# Respect cooldown if we recently saw ENOMEM
try:
# local import to save RAM
import urequests as requests # type: ignore
import gc # type: ignore
import time # type: ignore
now = time.time()
if _NEXT_ALLOWED_SEND_TS and now < _NEXT_ALLOWED_SEND_TS:
if debug: print("DBG: backing off until", _NEXT_ALLOWED_SEND_TS)
return False
except:
pass
try:
# Lightweight local imports and GC
import gc # type: ignore
import time # type: ignore
gc.collect()
# Quick mem check before importing urequests/SSL
mem = getattr(gc, "mem_free", lambda: None)()
# Require larger headroom based on device testing (adjust if you re-test)
if mem is not None and mem < 95000:
print("Discord send skipped: ENOMEM ({} bytes free)".format(mem))
return False
# Import urequests only when we plan to send
try:
import urequests as requests # type: ignore
except Exception as e:
print("Discord send failed: urequests import error:", e)
try:
_NEXT_ALLOWED_SEND_TS = time.time() + 60
except:
pass
return False
gc.collect()
if debug:
try: print("DBG: mem after import:", gc.mem_free() // 1024, "KB")
except: pass
# Build tiny payload
url = str(url).strip().strip('\'"')
content = _escape_json_str(message)
user = _escape_json_str(username)
content = _escape_json_str(str(message)[:140])
user = _escape_json_str(str(username)[:32])
body_bytes = ('{"content":"%s","username":"%s"}' % (content, user)).encode("utf-8")
headers = {"Content-Type": "application/json; charset=utf-8"}
headers = {"Content-Type": "application/json"}
resp = requests.post(url, data=body_bytes, headers=headers)
status = getattr(resp, "status", getattr(resp, "status_code", None))
success = bool(status and 200 <= status < 300)
if not success:
# optional: print status for debugging, but avoid spamming
print("Discord webhook failed, status:", status)
return success
return bool(status and 200 <= status < 300)
except Exception as e:
# avoid raising to prevent crashing monitors; print minimal info
print("Discord webhook exception:", e)
print("Discord send failed:", e)
try:
if ("ENOMEM" in str(e)) or isinstance(e, MemoryError):
import time # type: ignore
_NEXT_ALLOWED_SEND_TS = time.time() + 60
except:
pass
return False
finally:
@@ -66,12 +108,29 @@ def send_discord_message(message, username="Auto Garden Bot", is_alert=False):
resp.close()
except:
pass
# free large objects and modules, then force GC
# remove local refs and unload heavy modules to free peak RAM (urequests, ussl/ssl)
try:
del resp
if 'resp' in locals(): del resp
if 'body_bytes' in locals(): del body_bytes
if 'content' in locals(): del content
if 'user' in locals(): del user
if 'headers' in locals(): del headers
if 'requests' in locals(): del requests
except:
pass
try:
import sys
# remove urequests and SSL modules from module cache so their memory can be reclaimed
for m in ('urequests', 'ussl', 'ssl'):
if m in sys.modules:
try:
del sys.modules[m]
except:
pass
except:
pass
try:
import gc # type: ignore
gc.collect()
except:
pass

View File

@@ -58,12 +58,6 @@ class TempWebServer:
bytes_read = len(body_so_far.encode('utf-8'))
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!)
if bytes_needed > 0:
remaining_parts = []
@@ -77,21 +71,10 @@ class TempWebServer:
break
remaining_parts.append(chunk)
total_read += len(chunk)
print("DEBUG POST: Read {} bytes (total: {}/{})".format(
len(chunk), total_read, bytes_needed))
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')
# ===== 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:
response = self._handle_update(request, sensors, ac_monitor, heater_monitor, schedule_monitor, config)
# If error page redirects, handle it
@@ -118,7 +101,6 @@ class TempWebServer:
for i in range(0, len(response_bytes), chunk_size):
chunk = response_bytes[i:i+chunk_size]
conn.sendall(chunk)
print("DEBUG: Sent chunk {} ({} bytes)".format(i//chunk_size + 1, len(chunk)))
conn.close()
print("DEBUG: Schedule editor page sent successfully ({} bytes total)".format(len(response_bytes)))
@@ -212,6 +194,8 @@ class TempWebServer:
print("ERROR: Failed to send response: {}".format(e))
finally:
conn.close()
import gc # type: ignore
gc.collect()
print("DEBUG: Client connection closed")
# ===== END: Send response =====
@@ -284,7 +268,8 @@ class TempWebServer:
def _handle_schedule_update(self, request, sensors, ac_monitor, heater_monitor, schedule_monitor, config):
"""Handle schedule form submission."""
import gc # type: ignore
gc.collect()
try:
body = request.split('\r\n\r\n')[1] if '\r\n\r\n' in request else ''
params = {}
@@ -298,6 +283,7 @@ class TempWebServer:
mode_action = params.get('mode_action', '')
if mode_action == 'resume':
gc.collect()
# Resume automatic scheduling
config['schedule_enabled'] = True
config['permanent_hold'] = False
@@ -331,6 +317,7 @@ class TempWebServer:
return redirect_response
elif mode_action == 'temporary_hold':
gc.collect()
# Enter temporary hold (pause schedules temporarily)
config['schedule_enabled'] = False
config['permanent_hold'] = False
@@ -355,6 +342,7 @@ class TempWebServer:
return redirect_response
elif mode_action == 'permanent_hold':
gc.collect()
# Enter permanent hold (disable schedules permanently)
config['schedule_enabled'] = False
config['permanent_hold'] = True
@@ -380,6 +368,7 @@ class TempWebServer:
return redirect_response
elif mode_action == 'save_schedules':
gc.collect()
# Just fall through to schedule parsing below
pass
# ===== END: Handle mode actions =====
@@ -389,12 +378,6 @@ class TempWebServer:
prev_schedules = prev.get('schedules', [])
# ===== START: Handle schedule configuration save =====
# DEBUG: Print what we received
print("DEBUG: Received POST body parameters:")
for key, value in params.items():
print(" {} = '{}'".format(key, value))
print("DEBUG: Total params received: {}".format(len(params)))
# Parse schedules (4 slots)
schedules = []
has_any_schedule_data = False
@@ -501,8 +484,6 @@ class TempWebServer:
'heater_target': heater_target
}
schedules.append(schedule)
print("DEBUG: Parsed schedule {}: time='{}', name='{}', heater={}, ac={}".format(
i, schedule_time, schedule_name, heater_target, ac_target))
# Only update schedules if user submitted schedule form data
if has_any_schedule_data:
@@ -556,7 +537,8 @@ class TempWebServer:
if heater_monitor:
heater_monitor.target_temp = config['heater_target']
heater_monitor.temp_swing = config['heater_swing']
del params, prev_schedules, prev
gc.collect()
# Send Discord notification
try:
mode = "automatic" if config.get('schedule_enabled') else "hold"
@@ -567,7 +549,8 @@ class TempWebServer:
except:
pass
# ===== END: Handle schedule configuration save =====
del schedules
gc.collect()
# Redirect back to homepage with cache-busting headers
redirect_response = 'HTTP/1.1 303 See Other\r\n'
redirect_response += 'Location: /\r\n'
@@ -578,6 +561,7 @@ class TempWebServer:
redirect_response += 'Expires: 0\r\n'
redirect_response += '\r\n'
print("DEBUG: Returning redirect to dashboard (with cache-busting)")
gc.collect()
return redirect_response
except Exception as e:
@@ -1361,6 +1345,8 @@ document.addEventListener('DOMContentLoaded', function() {{
def _get_schedule_editor_page(self, sensors, ac_monitor, heater_monitor):
"""Generate schedule editor page (no auto-refresh, schedules only)."""
# Get current temps (read if not cached)
import gc # type: ignore
gc.collect()
inside_temp = getattr(sensors.get('inside'), 'last_temp', None)
if inside_temp is None:
inside_temps = sensors['inside'].read_all_temps(unit='F')
@@ -1400,8 +1386,6 @@ document.addEventListener('DOMContentLoaded', function() {{
# Build schedule inputs
schedule_inputs = ""
for i, schedule in enumerate(schedules[:4]):
print("DEBUG: Building HTML for schedule {}...".format(i))
time_value = schedule.get('time', '')
name_value = schedule.get('name', '')
heater_value = schedule.get('heater_target', config.get('heater_target'))
@@ -1431,8 +1415,6 @@ document.addEventListener('DOMContentLoaded', function() {{
schedule_inputs += "<input type=\"number\" name=\"schedule_" + str(i) + "_ac\" value=\"" + str(ac_value) + "\" step=\"0.5\" min=\"60\" max=\"90\" required oninput=\"schedSync(" + str(i) + ", 'ac')\" onchange=\"schedSync(" + str(i) + ", 'ac')\">\n"
schedule_inputs += '</div>\n'
print("DEBUG: HTML generated, length now: {} bytes".format(len(schedule_inputs)))
html = """
<!DOCTYPE html>
<html>
@@ -1636,7 +1618,8 @@ document.addEventListener('DOMContentLoaded', function() {{
def _get_settings_page(self, sensors, ac_monitor, heater_monitor):
"""Generate advanced settings page."""
config = self._load_config()
import gc # type: ignore
gc.collect()
# Get temperatures (read if not cached)
inside_temp = getattr(sensors.get('inside'), 'last_temp', None)
if inside_temp is None:
@@ -1811,6 +1794,8 @@ document.addEventListener('DOMContentLoaded', function() {{
def _handle_settings_update(self, request, sensors, ac_monitor, heater_monitor, schedule_monitor, config):
"""Handle advanced settings update."""
import gc # type: ignore
gc.collect()
try:
body = request.split('\r\n\r\n')[1] if '\r\n\r\n' in request else ''
params = {}
@@ -1877,4 +1862,5 @@ document.addEventListener('DOMContentLoaded', function() {{
redirect_response += 'Content-Length: 0\r\n'
redirect_response += 'Connection: close\r\n'
redirect_response += '\r\n'
gc.collect()
return redirect_response

228
main.py
View File

@@ -22,14 +22,6 @@ except Exception as e:
# Import after WiFi reset
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) =====
def sync_ntp_time(timezone_offset):
"""
@@ -144,6 +136,11 @@ def load_config():
return default_config
# global variables for Discord webhook status
discord_sent = False
discord_send_attempts = 0
pending_discord_message = None
# Load configuration from file
config = load_config()
import scripts.discord_webhook as discord_webhook
@@ -200,15 +197,33 @@ if wifi and wifi.isconnected():
print(f"Web Interface: http://{ifconfig[0]}")
print("="*50 + "\n")
# Send startup notification to Discord (with timeout, non-blocking)
try:
success = discord_webhook.send_discord_message(f"Pico W online at http://{ifconfig[0]}")
if success:
# Try sending Discord webhook NOW, before creating other objects
gc.collect()
ram_free = gc.mem_free()
print(f"DEBUG: Free RAM before Discord send: {ram_free // 1024} KB")
mem_ok = ram_free > 95000
if mem_ok:
ok = discord_webhook.send_discord_message("Pico W online at http://{}".format(ifconfig[0]), debug=False)
if ok:
print("Discord startup notification sent")
discord_sent = True
else:
print("Discord startup notification failed (continuing anyway)")
except Exception as e:
print("Discord notification error: {}".format(e))
print("Discord startup notification failed")
pending_discord_message = "Pico W online at http://{}".format(ifconfig[0])
discord_send_attempts = 1
else:
print("Not enough memory for Discord startup notification")
pending_discord_message = "Pico W online at http://{}".format(ifconfig[0])
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
web_server = TempWebServer(port=80)
@@ -233,6 +248,8 @@ else:
print("="*50 + "\n")
# ===== END: WiFi Connection =====
# ===== START: Sensor Configuration =====
# Define all temperature sensors and their alert thresholds
SENSOR_CONFIG = {
@@ -344,47 +361,6 @@ print("="*50 + "\n")
check_memory_once()
# ===== 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("Press Ctrl+C to stop\n")
@@ -393,55 +369,119 @@ retry_ntp_attempts = 0
max_ntp_attempts = 5 # Try up to 5 times after initial failure
last_ntp_sync = time.time() # Track when we last synced
# ===== START: Main Loop =====
# Main monitoring loop (runs forever until Ctrl+C)
while True:
try:
# Run all monitors (each checks if it's time to run via should_run())
run_monitors(monitors)
try:
while True:
# Web requests
# ===== START: Main Loop =====
# 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:
now = time.time()
# WiFi monitor every 5 seconds (can be stateless)
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
# Schedule monitor every 60 seconds (persistent)
if now - last_monitor_run["schedule"] >= 60:
try:
schedule_monitor.run()
except Exception as e:
print("ScheduleMonitor error:", e)
last_monitor_run["schedule"] = now
# AC monitor every 30 seconds (persistent)
if now - last_monitor_run["ac"] >= 30:
try:
ac_monitor.run()
except Exception as e:
print("ACMonitor error:", e)
last_monitor_run["ac"] = now
# Heater monitor every 30 seconds (persistent)
if now - last_monitor_run["heater"] >= 30:
try:
heater_monitor.run()
except Exception as e:
print("HeaterMonitor error:", e)
last_monitor_run["heater"] = now
# Inside temperature monitor every 10 seconds (can be stateless)
if now - last_monitor_run["inside_temp"] >= 10:
from scripts.monitors import TemperatureMonitor
inside_monitor = TemperatureMonitor(
sensor=sensors['inside'],
label=SENSOR_CONFIG['inside']['label'],
check_interval=10,
report_interval=30,
alert_high=SENSOR_CONFIG['inside']['alert_high'],
alert_low=SENSOR_CONFIG['inside']['alert_low'],
log_file="/temp_logs.csv",
send_alerts_to_separate_channel=True
)
inside_monitor.run()
del inside_monitor
gc.collect()
last_monitor_run["inside_temp"] = now
# Outside temperature monitor every 10 seconds (can be stateless)
if now - last_monitor_run["outside_temp"] >= 10:
from scripts.monitors import TemperatureMonitor
outside_monitor = TemperatureMonitor(
sensor=sensors['outside'],
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)
# ===== PERIODIC RE-SYNC (every 24 hours) =====
if ntp_synced and (time.time() - last_ntp_sync) > 86400:
print("24-hour re-sync due...")
if sync_ntp_time(TIMEZONE_OFFSET):
last_ntp_sync = time.time()
print("Daily NTP re-sync successful")
else:
print("Daily NTP re-sync failed (will retry tomorrow)")
# ===== END: PERIODIC RE-SYNC =====
# ===== ADD THIS: AGGRESSIVE GARBAGE COLLECTION =====
current_time = time.time()
if int(current_time) % 5 == 0: # Every 5 seconds
gc.collect()
# Optional: Print memory stats occasionally
if int(current_time) % 60 == 0: # Every minute
print("💾 Memory free: {} KB".format(gc.mem_free() // 1024))
# ===== END: AGGRESSIVE GC =====
time.sleep(0.1)
except KeyboardInterrupt:
# Graceful shutdown on Ctrl+C
print("\n\n" + "="*50)
# ===== END: Main Loop =====
except KeyboardInterrupt:
print("\n" + "="*50)
print("Shutting down gracefully...")
print("="*50)
try:
print("Turning off AC...")
ac_controller.turn_off()
except Exception as e:
print("AC shutdown error:", e)
try:
print("Turning off heater...")
heater_controller.turn_off()
except Exception as e:
print("Heater shutdown error:", e)
try:
print("Turning off LED...")
led.low()
print("Shutdown complete!")
print("="*50 + "\n")
break
except Exception as e:
# If loop crashes, print error and keep running
print("❌ Main loop error: {}".format(e))
import sys
sys.print_exception(e)
time.sleep(5) # Brief pause before retrying
# ===== END: Main Loop =====
print("LED shutdown error:", e)
print("Shutdown complete!")
print("="*50)