Compare commits

...

3 Commits

Author SHA1 Message Date
b93809946a feat: Add caching for last temperature reading in TemperatureMonitor 2025-11-08 15:48:00 -05:00
f4c9e20836 feat: Implement watchdog timer and enhance NTP time synchronization with retry logic 2025-11-08 15:47:55 -05:00
9fda192f0b Bug: Enhance schedule handling with improved request processing and validation
Sometimes page loads, sometimes doesn't trying to implement something to figure out why the page isn't loading. In python everything loads in certain order so if something hangs, it could prevent something else from running. (Like web page from loading :()
2025-11-08 15:47:39 -05:00
3 changed files with 309 additions and 172 deletions

View File

@@ -55,6 +55,8 @@ class TemperatureMonitor(Monitor):
self.last_report = 0
self.alert_sent = False
self.alert_start_time = None # Track when alert started
self.last_temp = None # Cached Last temperature reading
self.last_read_time = 0 # Timestamp of last reading
def should_run(self):
"""Check if it's time to run this monitor."""
@@ -75,6 +77,10 @@ class TemperatureMonitor(Monitor):
temp = list(temps.values())[0] # Get first temp reading
# Cache the reading for web server (avoid blocking reads)
self.last_temp = temp
self.last_read_time = current_time
# Check for alerts
alert_condition = False
alert_message = ""

View File

@@ -8,6 +8,7 @@ class TempWebServer:
self.port = port
self.socket = None
self.sensors = {}
self.last_page_render = 0 # Track last successful HTML generation
def start(self):
"""Start the web server (non-blocking)."""
@@ -20,34 +21,47 @@ class TempWebServer:
print("Web server started on port {}".format(self.port))
except Exception as e:
print("Failed to start web server: {}".format(e))
def check_requests(self, sensors, ac_monitor=None, heater_monitor=None, schedule_monitor=None):
"""Check for incoming requests (call in main loop)."""
if not self.socket:
return
try:
conn, addr = self.socket.accept()
conn.settimeout(3.0)
request = conn.recv(1024).decode('utf-8')
# Check if this is a POST request (form submission)
if 'POST /update' in request:
response = self._handle_update(request, sensors, ac_monitor, heater_monitor, schedule_monitor)
elif 'GET /schedule' in request:
response = self._get_schedule_editor_page(sensors, ac_monitor, heater_monitor)
conn.send('HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\n\r\n')
conn.sendall(response.encode('utf-8'))
conn.close()
return
elif 'POST /schedule' in request:
response = self._handle_schedule_update(request, sensors, ac_monitor, heater_monitor, schedule_monitor)
# If handler returns a redirect response, send it raw and exit
if isinstance(response, str) and response.startswith('HTTP/1.1 303'):
conn.sendall(response.encode('utf-8'))
conn.close()
return
elif 'GET /ping' in request:
# Quick health check endpoint (no processing)
conn.send('HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\n')
conn.sendall(b'OK')
conn.close()
return
else:
# Regular GET request
response = self._get_status_page(sensors, ac_monitor, heater_monitor)
# Make sure we have a valid response
if response is None:
print("Error: response is None, generating default page")
response = self._get_status_page(sensors, ac_monitor, heater_monitor)
conn.send('HTTP/1.1 200 OK\r\n')
conn.send('Content-Type: text/html; charset=utf-8\r\n')
conn.send('Connection: close\r\n\r\n')
conn.send('HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\n\r\n')
conn.sendall(response.encode('utf-8'))
conn.close()
except OSError:
@@ -56,12 +70,24 @@ class TempWebServer:
print("Web server error: {}".format(e))
import sys
sys.print_exception(e)
def _save_config_to_file(self, config):
"""Save configuration to config.json file."""
"""Save configuration to config.json file (atomic write)."""
try:
with open('config.json', 'w') as f:
import os
# Write to temp file first
with open('config.tmp', 'w') as f:
json.dump(config, f)
# Remove old config if exists
try:
os.remove('config.json')
except:
pass
# Rename temp to config (atomic on most filesystems)
os.rename('config.tmp', 'config.json')
print("Settings saved to config.json")
return True
except Exception as e:
@@ -86,6 +112,7 @@ class TempWebServer:
def _handle_schedule_update(self, request, sensors, ac_monitor, heater_monitor, schedule_monitor):
"""Handle schedule form submission."""
try:
body = request.split('\r\n\r\n')[1] if '\r\n\r\n' in request else ''
params = {}
@@ -160,7 +187,6 @@ class TempWebServer:
# Redirect back to homepage
return 'HTTP/1.1 303 See Other\r\nLocation: /\r\n\r\n'
# ===== END: Handle mode actions =====
elif mode_action == 'save_schedules':
# Just fall through to schedule parsing below
@@ -177,6 +203,20 @@ class TempWebServer:
heater_key = 'schedule_{}_heater'.format(i)
if time_key in params and params[time_key]:
# Validate time format (HH:MM)
time_val = params[time_key]
if ':' not in time_val or len(time_val.split(':')) != 2:
print("Invalid time format: {}".format(time_val))
return 'HTTP/1.1 303 See Other\r\nLocation: /schedule\r\n\r\n'
try:
hours, mins = time_val.split(':')
if not (0 <= int(hours) <= 23 and 0 <= int(mins) <= 59):
raise ValueError
except:
print("Invalid time value: {}".format(time_val))
return 'HTTP/1.1 303 See Other\r\nLocation: /schedule\r\n\r\n'
schedule = {
'time': params[time_key],
'name': params.get(name_key, 'Schedule {}'.format(i+1)),
@@ -223,13 +263,16 @@ class TempWebServer:
except:
pass
# ===== END: Handle schedule configuration save =====
# Redirect back to schedule page
return 'HTTP/1.1 303 See Other\r\nLocation: /schedule\r\n\r\n'
except Exception as e:
print("Error updating schedule: {}".format(e))
import sys
sys.print_exception(e)
return self._get_status_page(sensors, ac_monitor, heater_monitor, show_success=True)
# Safety: avoid rendering an error page here; just redirect
return 'HTTP/1.1 303 See Other\r\nLocation: /schedule\r\n\r\n'
def _handle_update(self, request, sensors, ac_monitor, heater_monitor, schedule_monitor):
"""Handle form submission and update settings."""
@@ -342,12 +385,18 @@ class TempWebServer:
"""Generate HTML status page."""
print("DEBUG: Generating status page...")
try:
# Get current temperatures
inside_temps = sensors['inside'].read_all_temps(unit='F')
outside_temps = sensors['outside'].read_all_temps(unit='F')
# Get current temperatures (use cached values to avoid blocking)
inside_temp = getattr(sensors.get('inside'), 'last_temp', None)
outside_temp = getattr(sensors.get('outside'), 'last_temp', None)
inside_temp = list(inside_temps.values())[0] if inside_temps else "N/A"
outside_temp = list(outside_temps.values())[0] if outside_temps else "N/A"
# Fallback to sensor read if no cached value (first load only)
if inside_temp is None:
inside_temps = sensors['inside'].read_all_temps(unit='F')
inside_temp = list(inside_temps.values())[0] if inside_temps else "N/A"
if outside_temp is None:
outside_temps = sensors['outside'].read_all_temps(unit='F')
outside_temp = list(outside_temps.values())[0] if outside_temps else "N/A"
# Get AC/Heater status
ac_status = "ON" if ac_monitor and ac_monitor.ac.get_state() else "OFF"
@@ -386,6 +435,9 @@ class TempWebServer:
# Build schedule cards
schedule_cards = ""
# Build mode buttons for dashboard
mode_buttons = self._build_mode_buttons(config)
if config.get('schedules'):
for schedule in config.get('schedules', []):
# ===== START: Decode URL-encoded values =====
@@ -416,9 +468,6 @@ class TempWebServer:
</div>
"""
# Build schedule form
schedule_form = self._build_schedule_form(config)
# Success message
success_html = """
<div class="success-message">
@@ -732,7 +781,13 @@ class TempWebServer:
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px;">
{schedule_cards}
</div>
{schedule_form}
{mode_buttons}
<div style="margin-top: 20px; text-align: center;">
<a href="/schedule" class="btn" style="text-decoration: none; display: inline-block;">
⚙️ Edit Schedules
</a>
</div>
</div>
<div class="footer">
@@ -759,8 +814,9 @@ class TempWebServer:
schedule_color=schedule_color,
schedule_icon=schedule_icon,
schedule_cards=schedule_cards,
schedule_form=schedule_form
mode_buttons=mode_buttons
)
self.last_page_render = time.time() # Track successful render
return html
except Exception as e:
@@ -771,13 +827,11 @@ class TempWebServer:
def _get_error_page(self, error_title, error_message, sensors, ac_monitor, heater_monitor):
"""Generate error page with message."""
# Get current temps
inside_temps = sensors['inside'].read_all_temps(unit='F')
outside_temps = sensors['outside'].read_all_temps(unit='F')
inside_temp = list(inside_temps.values())[0] if inside_temps else "N/A"
outside_temp = list(outside_temps.values())[0] if outside_temps else "N/A"
# Get current temps (cached, fast - no blocking sensor reads)
inside_temp = getattr(sensors.get('inside'), 'last_temp', None) or "N/A"
outside_temp = getattr(sensors.get('outside'), 'last_temp', None) or "N/A"
# Format temperature values
inside_temp_str = "{:.1f}".format(inside_temp) if isinstance(inside_temp, float) else str(inside_temp)
outside_temp_str = "{:.1f}".format(outside_temp) if isinstance(outside_temp, float) else str(outside_temp)
@@ -896,137 +950,197 @@ class TempWebServer:
)
return html
def _build_schedule_form(self, config):
"""Build the schedule editing form."""
def _get_schedule_editor_page(self, sensors, ac_monitor, heater_monitor):
"""Generate schedule editor page (no auto-refresh, schedules only)."""
# Get current temps (use cached to avoid blocking)
inside_temp = getattr(sensors.get('inside'), 'last_temp', None) or "N/A"
outside_temp = getattr(sensors.get('outside'), 'last_temp', None) or "N/A"
# Format temperature values
inside_temp_str = "{:.1f}".format(inside_temp) if isinstance(inside_temp, float) else str(inside_temp)
outside_temp_str = "{:.1f}".format(outside_temp) if isinstance(outside_temp, float) else str(outside_temp)
# Load config
config = self._load_config()
schedules = config.get('schedules', [])
# Pad with empty schedules up to 4
while len(schedules) < 4:
schedules.append({'time': '', 'name': '', 'ac_target': 77.0, 'heater_target': 80.0})
# ===== START: Determine current mode =====
# Check if schedules exist
has_schedules = len([s for s in schedules if s.get('time')]) > 0
# Determine mode based on config
if not has_schedules:
current_mode = "no_schedules" # No schedules configured yet
elif config.get('schedule_enabled'):
current_mode = "automatic" # Schedules are running
elif config.get('permanent_hold', False):
current_mode = "permanent_hold" # User disabled schedules permanently
else:
current_mode = "temporary_hold" # Manual override (HOLD mode)
# ===== END: Determine current mode =====
# ===== START: Build mode control buttons =====
if current_mode == "no_schedules":
# No mode buttons if no schedules configured
mode_buttons = """
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; text-align: center; color: #7f8c8d; margin-bottom: 20px;">
Configure schedules below, then choose a mode
</div>
"""
elif current_mode == "automatic":
# Automatic mode active
mode_buttons = """
<div style="background: linear-gradient(135deg, #2ecc71, #27ae60); color: white; padding: 15px; border-radius: 10px; margin-bottom: 20px; box-shadow: 0 4px 8px rgba(0,0,0,0.2);">
<div style="font-weight: bold; font-size: 18px; margin-bottom: 10px;">
✅ Automatic Mode Active
</div>
<div style="font-size: 14px; margin-bottom: 15px;">
Temperatures automatically adjust based on schedule
</div>
<div style="display: flex; gap: 10px; justify-content: center;">
<button type="submit" name="mode_action" value="temporary_hold" style="padding: 8px 16px; background: #f39c12; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">
⏸️ Temporary Hold
</button>
<button type="submit" name="mode_action" value="permanent_hold" style="padding: 8px 16px; background: #e74c3c; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">
🛑 Permanent Hold
</button>
</div>
</div>
"""
elif current_mode == "temporary_hold":
# Temporary hold (manual override)
mode_buttons = """
<div style="background: linear-gradient(135deg, #f39c12, #e67e22); color: white; padding: 15px; border-radius: 10px; margin-bottom: 20px; box-shadow: 0 4px 8px rgba(0,0,0,0.2);">
<div style="font-weight: bold; font-size: 18px; margin-bottom: 10px;">
⏸️ Temporary Hold Active
</div>
<div style="font-size: 14px; margin-bottom: 15px;">
Manual settings in use - Schedule paused
</div>
<div style="display: flex; gap: 10px; justify-content: center;">
<button type="submit" name="mode_action" value="resume" style="padding: 8px 16px; background: #2ecc71; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">
▶️ Resume Schedule
</button>
<button type="submit" name="mode_action" value="permanent_hold" style="padding: 8px 16px; background: #e74c3c; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">
🛑 Make Permanent
</button>
</div>
</div>
"""
else: # permanent_hold
# Permanent hold (schedules disabled by user)
mode_buttons = """
<div style="background: linear-gradient(135deg, #e74c3c, #c0392b); color: white; padding: 15px; border-radius: 10px; margin-bottom: 20px; box-shadow: 0 4px 8px rgba(0,0,0,0.2);">
<div style="font-weight: bold; font-size: 18px; margin-bottom: 10px;">
🛑 Permanent Hold Active
</div>
<div style="font-size: 14px; margin-bottom: 15px;">
Schedules disabled - Manual control only
</div>
<div style="display: flex; gap: 10px; justify-content: center;">
<button type="submit" name="mode_action" value="resume" style="padding: 8px 16px; background: #2ecc71; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">
▶️ Enable Schedules
</button>
</div>
</div>
"""
# ===== END: Build mode control buttons =====
form = """
<form method="POST" action="/schedule" class="controls" style="margin-top: 20px;">
<h3 style="color: #34495e; margin-bottom: 15px;">⚙️ Schedule Configuration</h3>
{mode_buttons}
""".format(mode_buttons=mode_buttons)
schedules.append({'time': '', 'name': '', 'ac_target': 75.0, 'heater_target': 72.0})
# Build schedule inputs
schedule_inputs = ""
for i, schedule in enumerate(schedules[:4]):
form += """
<div class="schedule-row">
<div class="control-group" style="margin: 0;">
<label class="control-label" style="font-size: 14px;">Time</label>
<input type="time" name="schedule_{i}_time" value="{time}">
schedule_inputs += """
<div style="display: grid; grid-template-columns: 1fr 2fr 1fr 1fr; gap: 10px; margin-bottom: 10px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
<div>
<label style="font-size: 14px; font-weight: bold; color: #34495e; display: block; margin-bottom: 5px;">Time</label>
<input type="time" name="schedule_{i}_time" value="{time}" style="width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 5px;">
</div>
<div class="control-group" style="margin: 0;">
<label class="control-label" style="font-size: 14px;">Name</label>
<input type="text" name="schedule_{i}_name" value="{name}" placeholder="e.g. Morning">
<div>
<label style="font-size: 14px; font-weight: bold; color: #34495e; display: block; margin-bottom: 5px;">Name</label>
<input type="text" name="schedule_{i}_name" value="{name}" placeholder="e.g. Morning" style="width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 5px;">
</div>
<div class="control-group" style="margin: 0;">
<label class="control-label" style="font-size: 14px;">Heater (°F)</label>
<input type="number" name="schedule_{i}_heater" value="{heater}" step="0.5" min="60" max="85">
<div>
<label style="font-size: 14px; font-weight: bold; color: #34495e; display: block; margin-bottom: 5px;">🔥 Heater (°F)</label>
<input type="number" name="schedule_{i}_heater" value="{heater}" step="0.5" min="60" max="85" style="width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 5px;">
</div>
<div class="control-group" style="margin: 0;">
<label class="control-label" style="font-size: 14px;">AC (°F)</label>
<input type="number" name="schedule_{i}_ac" value="{ac}" step="0.5" min="60" max="85">
<div>
<label style="font-size: 14px; font-weight: bold; color: #34495e; display: block; margin-bottom: 5px;">❄️ AC (°F)</label>
<input type="number" name="schedule_{i}_ac" value="{ac}" step="0.5" min="60" max="85" style="width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 5px;">
</div>
</div>
""".format(
i=i,
time=schedule.get('time', ''),
name=schedule.get('name', ''),
ac=schedule.get('ac_target', 77.0),
heater=schedule.get('heater_target', 80.0)
heater=schedule.get('heater_target', 72.0),
ac=schedule.get('ac_target', 75.0)
)
form += """
<div class="control-group" style="margin-top: 20px;">
<button type="submit" name="mode_action" value="save_schedules" class="btn">💾 Save Schedule Changes</button>
html = """
<!DOCTYPE html>
<html>
<head>
<title>Schedule Editor - Climate Control</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8">
<style>
body {{
font-family: Arial, sans-serif;
max-width: 1000px;
margin: 0 auto;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}}
.container {{
background: white;
border-radius: 15px;
padding: 30px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}}
h1 {{
color: #2c3e50;
text-align: center;
margin-bottom: 20px;
}}
.header-info {{
display: flex;
justify-content: center;
gap: 30px;
margin-bottom: 30px;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
}}
.btn {{
padding: 12px 24px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 8px;
font-weight: bold;
cursor: pointer;
font-size: 16px;
text-decoration: none;
display: inline-block;
}}
.btn:hover {{ transform: translateY(-2px); }}
</style>
</head>
<body>
<div class="container">
<h1>📅 Schedule Configuration</h1>
<div class="header-info">
<div>🏠 Inside: <strong>{inside_temp}°F</strong></div>
<div>🌡️ Outside: <strong>{outside_temp}°F</strong></div>
</div>
</form>
"""
<form method="POST" action="/schedule">
<h3 style="color: #34495e; margin-bottom: 15px;">⏰ Configure Schedule Times & Temperatures</h3>
<p style="color: #7f8c8d; margin-bottom: 20px;">
Set up to 4 time-based schedules. Leave time blank to disable a schedule.
</p>
{schedule_inputs}
<div style="margin-top: 20px;">
<button type="submit" name="mode_action" value="save_schedules" class="btn" style="width: 100%;">
💾 Save Schedule Configuration
</button>
</div>
</form>
<div style="text-align: center; margin-top: 20px;">
<a href="/" class="btn" style="background: linear-gradient(135deg, #95a5a6, #7f8c8d);">
⬅️ Back to Dashboard
</a>
</div>
<div style="text-align: center; color: #7f8c8d; margin-top: 20px; padding-top: 20px; border-top: 2px solid #ecf0f1;">
💡 This page does not auto-refresh<br>
To change modes (Automatic/Hold), return to the dashboard
</div>
</div>
</body>
</html>
""".format(
inside_temp=inside_temp_str,
outside_temp=outside_temp_str,
schedule_inputs=schedule_inputs
)
return form
return html
def _build_mode_buttons(self, config):
"""Build mode control buttons for dashboard only."""
schedules = config.get('schedules', [])
has_schedules = len([s for s in schedules if s.get('time')]) > 0
if not has_schedules:
return """
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; text-align: center; color: #7f8c8d; margin: 20px 0;">
No schedules configured - <a href="/schedule" style="color: #667eea; font-weight: bold;">Configure schedules</a>
</div>
"""
# Build mode buttons based on current state
if config.get('schedule_enabled'):
return """
<form method="POST" action="/schedule" style="margin: 20px 0;">
<div style="background: linear-gradient(135deg, #2ecc71, #27ae60); color: white; padding: 15px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.2);">
<div style="font-weight: bold; font-size: 18px; margin-bottom: 10px;">✅ Automatic Mode</div>
<div style="font-size: 14px; margin-bottom: 15px;">Temperatures adjust based on schedule</div>
<div style="display: flex; gap: 10px; justify-content: center;">
<button type="submit" name="mode_action" value="temporary_hold" style="padding: 10px 20px; background: #f39c12; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">⏸️ Pause</button>
<button type="submit" name="mode_action" value="permanent_hold" style="padding: 10px 20px; background: #e74c3c; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">🛑 Disable</button>
</div>
</div>
</form>
"""
elif config.get('permanent_hold', False):
return """
<form method="POST" action="/schedule" style="margin: 20px 0;">
<div style="background: linear-gradient(135deg, #e74c3c, #c0392b); color: white; padding: 15px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.2);">
<div style="font-weight: bold; font-size: 18px; margin-bottom: 10px;">🛑 Permanent Hold</div>
<div style="font-size: 14px; margin-bottom: 15px;">Manual control only - Schedules disabled</div>
<button type="submit" name="mode_action" value="resume" style="padding: 10px 20px; background: #2ecc71; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">▶️ Enable Schedules</button>
</div>
</form>
"""
else:
return """
<form method="POST" action="/schedule" style="margin: 20px 0;">
<div style="background: linear-gradient(135deg, #f39c12, #e67e22); color: white; padding: 15px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.2);">
<div style="font-weight: bold; font-size: 18px; margin-bottom: 10px;">⏸️ Temporary Hold</div>
<div style="font-size: 14px; margin-bottom: 15px;">Manual override active</div>
<div style="display: flex; gap: 10px; justify-content: center;">
<button type="submit" name="mode_action" value="resume" style="padding: 10px 20px; background: #2ecc71; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">▶️ Resume</button>
<button type="submit" name="mode_action" value="permanent_hold" style="padding: 10px 20px; background: #e74c3c; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">🛑 Disable</button>
</div>
</div>
</form>
"""

61
main.py
View File

@@ -1,9 +1,13 @@
from machine import Pin
from machine import Pin, WDT
import time
import network
import json
import gc # ADD THIS - for garbage collection
# Enable watchdog (8 seconds timeout - auto-reboot if frozen)
wdt = WDT(timeout=60000)
print("Watchdog enabled (60s timeout)")
# Initialize pins (LED light onboard)
led = Pin("LED", Pin.OUT)
led.low()
@@ -124,16 +128,20 @@ if wifi and wifi.isconnected():
# Send startup notification to Discord
send_discord_message(f"Pico W online at http://{ifconfig[0]}")
# ===== START: NTP Time Sync =====
# Sync time with internet time server (required for schedules to work correctly)
# Without this, the Pico's clock starts at 2021 on every reboot
# Start web server early so page can load even if time sync is slow
web_server = TempWebServer(port=80)
web_server.start()
# Attempt time sync non-blocking (short timeout + retry flag)
ntp_synced = False
try:
import ntptime
ntptime.settime() # Downloads current time from pool.ntp.org
ntptime.settime()
ntp_synced = True
print("Time synced with NTP server")
except Exception as e:
print("Failed to sync time: {}".format(e))
# ===== END: NTP Time Sync =====
print("Initial NTP sync failed: {}".format(e))
# Will retry later in loop
else:
# WiFi connection failed
@@ -142,12 +150,6 @@ else:
print("="*50 + "\n")
# ===== END: WiFi Connection =====
# ===== START: Web Server Setup =====
# Start web server for monitoring and control (accessible at http://192.168.86.43)
web_server = TempWebServer(port=80)
web_server.start()
# ===== END: Web Server Setup =====
# ===== START: Sensor Configuration =====
# Define all temperature sensors and their alert thresholds
SENSOR_CONFIG = {
@@ -285,21 +287,36 @@ monitors = [
print("Starting monitoring loop...")
print("Press Ctrl+C to stop\n")
# Add NTP retry flags (before main loop)
retry_ntp_attempts = 0
max_ntp_attempts = 5 # Try up to 5 times after initial failure
# ===== START: Main Loop =====
# Main monitoring loop (runs forever until Ctrl+C)
while True:
# Run all monitors (each checks if it's time to run via should_run())
run_monitors(monitors)
# Check for incoming web requests (non-blocking)
# Pass schedule_monitor so web interface can reload config when schedules change
# Web requests
web_server.check_requests(sensors, ac_monitor, heater_monitor, schedule_monitor)
# ===== START: Garbage Collection =====
# Free up unused memory to prevent fragmentation
# Retry NTP sync every ~10s if not yet synced
if not ntp_synced and retry_ntp_attempts < max_ntp_attempts:
# Try once immediately, then whenever (time.time() % 10) < 1 (rough 10s window)
try:
import ntptime
if retry_ntp_attempts == 0 or (time.time() % 10) < 1:
ntptime.settime()
ntp_synced = True
print("NTP sync succeeded on retry #{}".format(retry_ntp_attempts + 1))
except Exception as e:
# Increment only when an actual attempt was made
if retry_ntp_attempts == 0 or (time.time() % 10) < 1:
retry_ntp_attempts += 1
print("NTP retry {} failed: {}".format(retry_ntp_attempts, e))
gc.collect()
# ===== END: Garbage Collection =====
# Small delay to prevent CPU overload (0.1 seconds = 10 loops per second)
wdt.feed() # Reset watchdog timer (prevent auto-reboot)
time.sleep(0.1)
# ===== END: Main Loop =====