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.last_report = 0
self.alert_sent = False self.alert_sent = False
self.alert_start_time = None # Track when alert started 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): def should_run(self):
"""Check if it's time to run this monitor.""" """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 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 # Check for alerts
alert_condition = False alert_condition = False
alert_message = "" alert_message = ""

View File

@@ -8,6 +8,7 @@ class TempWebServer:
self.port = port self.port = port
self.socket = None self.socket = None
self.sensors = {} self.sensors = {}
self.last_page_render = 0 # Track last successful HTML generation
def start(self): def start(self):
"""Start the web server (non-blocking).""" """Start the web server (non-blocking)."""
@@ -20,34 +21,47 @@ class TempWebServer:
print("Web server started on port {}".format(self.port)) print("Web server started on port {}".format(self.port))
except Exception as e: except Exception as e:
print("Failed to start web server: {}".format(e)) print("Failed to start web server: {}".format(e))
def check_requests(self, sensors, ac_monitor=None, heater_monitor=None, schedule_monitor=None): def check_requests(self, sensors, ac_monitor=None, heater_monitor=None, schedule_monitor=None):
"""Check for incoming requests (call in main loop).""" """Check for incoming requests (call in main loop)."""
if not self.socket: if not self.socket:
return return
try: try:
conn, addr = self.socket.accept() conn, addr = self.socket.accept()
conn.settimeout(3.0) conn.settimeout(3.0)
request = conn.recv(1024).decode('utf-8') request = conn.recv(1024).decode('utf-8')
# Check if this is a POST request (form submission)
if 'POST /update' in request: if 'POST /update' in request:
response = self._handle_update(request, sensors, ac_monitor, heater_monitor, schedule_monitor) 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: elif 'POST /schedule' in request:
response = self._handle_schedule_update(request, sensors, ac_monitor, heater_monitor, schedule_monitor) 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: else:
# Regular GET request
response = self._get_status_page(sensors, ac_monitor, heater_monitor) response = self._get_status_page(sensors, ac_monitor, heater_monitor)
# Make sure we have a valid response
if response is None: if response is None:
print("Error: response is None, generating default page")
response = self._get_status_page(sensors, ac_monitor, heater_monitor) response = self._get_status_page(sensors, ac_monitor, heater_monitor)
conn.send('HTTP/1.1 200 OK\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.send('Content-Type: text/html; charset=utf-8\r\n')
conn.send('Connection: close\r\n\r\n')
conn.sendall(response.encode('utf-8')) conn.sendall(response.encode('utf-8'))
conn.close() conn.close()
except OSError: except OSError:
@@ -56,12 +70,24 @@ class TempWebServer:
print("Web server error: {}".format(e)) print("Web server error: {}".format(e))
import sys import sys
sys.print_exception(e) sys.print_exception(e)
def _save_config_to_file(self, config): def _save_config_to_file(self, config):
"""Save configuration to config.json file.""" """Save configuration to config.json file (atomic write)."""
try: 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) 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") print("Settings saved to config.json")
return True return True
except Exception as e: except Exception as e:
@@ -86,6 +112,7 @@ class TempWebServer:
def _handle_schedule_update(self, request, sensors, ac_monitor, heater_monitor, schedule_monitor): def _handle_schedule_update(self, request, sensors, ac_monitor, heater_monitor, schedule_monitor):
"""Handle schedule form submission.""" """Handle schedule form submission."""
try: try:
body = request.split('\r\n\r\n')[1] if '\r\n\r\n' in request else '' body = request.split('\r\n\r\n')[1] if '\r\n\r\n' in request else ''
params = {} params = {}
@@ -160,7 +187,6 @@ class TempWebServer:
# Redirect back to homepage # Redirect back to homepage
return 'HTTP/1.1 303 See Other\r\nLocation: /\r\n\r\n' return 'HTTP/1.1 303 See Other\r\nLocation: /\r\n\r\n'
# ===== END: Handle mode actions =====
elif mode_action == 'save_schedules': elif mode_action == 'save_schedules':
# Just fall through to schedule parsing below # Just fall through to schedule parsing below
@@ -177,6 +203,20 @@ class TempWebServer:
heater_key = 'schedule_{}_heater'.format(i) heater_key = 'schedule_{}_heater'.format(i)
if time_key in params and params[time_key]: 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 = { schedule = {
'time': params[time_key], 'time': params[time_key],
'name': params.get(name_key, 'Schedule {}'.format(i+1)), 'name': params.get(name_key, 'Schedule {}'.format(i+1)),
@@ -223,13 +263,16 @@ class TempWebServer:
except: except:
pass pass
# ===== END: Handle schedule configuration save ===== # ===== 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: except Exception as e:
print("Error updating schedule: {}".format(e)) print("Error updating schedule: {}".format(e))
import sys import sys
sys.print_exception(e) sys.print_exception(e)
# Safety: avoid rendering an error page here; just redirect
return self._get_status_page(sensors, ac_monitor, heater_monitor, show_success=True) 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): def _handle_update(self, request, sensors, ac_monitor, heater_monitor, schedule_monitor):
"""Handle form submission and update settings.""" """Handle form submission and update settings."""
@@ -342,12 +385,18 @@ class TempWebServer:
"""Generate HTML status page.""" """Generate HTML status page."""
print("DEBUG: Generating status page...") print("DEBUG: Generating status page...")
try: try:
# Get current temperatures # Get current temperatures (use cached values to avoid blocking)
inside_temps = sensors['inside'].read_all_temps(unit='F') inside_temp = getattr(sensors.get('inside'), 'last_temp', None)
outside_temps = sensors['outside'].read_all_temps(unit='F') outside_temp = getattr(sensors.get('outside'), 'last_temp', None)
inside_temp = list(inside_temps.values())[0] if inside_temps else "N/A" # Fallback to sensor read if no cached value (first load only)
outside_temp = list(outside_temps.values())[0] if outside_temps else "N/A" 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 # Get AC/Heater status
ac_status = "ON" if ac_monitor and ac_monitor.ac.get_state() else "OFF" ac_status = "ON" if ac_monitor and ac_monitor.ac.get_state() else "OFF"
@@ -386,6 +435,9 @@ class TempWebServer:
# Build schedule cards # Build schedule cards
schedule_cards = "" schedule_cards = ""
# Build mode buttons for dashboard
mode_buttons = self._build_mode_buttons(config)
if config.get('schedules'): if config.get('schedules'):
for schedule in config.get('schedules', []): for schedule in config.get('schedules', []):
# ===== START: Decode URL-encoded values ===== # ===== START: Decode URL-encoded values =====
@@ -416,9 +468,6 @@ class TempWebServer:
</div> </div>
""" """
# Build schedule form
schedule_form = self._build_schedule_form(config)
# Success message # Success message
success_html = """ success_html = """
<div class="success-message"> <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;"> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px;">
{schedule_cards} {schedule_cards}
</div> </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>
<div class="footer"> <div class="footer">
@@ -759,8 +814,9 @@ class TempWebServer:
schedule_color=schedule_color, schedule_color=schedule_color,
schedule_icon=schedule_icon, schedule_icon=schedule_icon,
schedule_cards=schedule_cards, schedule_cards=schedule_cards,
schedule_form=schedule_form mode_buttons=mode_buttons
) )
self.last_page_render = time.time() # Track successful render
return html return html
except Exception as e: except Exception as e:
@@ -771,13 +827,11 @@ class TempWebServer:
def _get_error_page(self, error_title, error_message, sensors, ac_monitor, heater_monitor): def _get_error_page(self, error_title, error_message, sensors, ac_monitor, heater_monitor):
"""Generate error page with message.""" """Generate error page with message."""
# Get current temps # Get current temps (cached, fast - no blocking sensor reads)
inside_temps = sensors['inside'].read_all_temps(unit='F') inside_temp = getattr(sensors.get('inside'), 'last_temp', None) or "N/A"
outside_temps = sensors['outside'].read_all_temps(unit='F') outside_temp = getattr(sensors.get('outside'), 'last_temp', None) or "N/A"
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"
# Format temperature values
inside_temp_str = "{:.1f}".format(inside_temp) if isinstance(inside_temp, float) else str(inside_temp) 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) outside_temp_str = "{:.1f}".format(outside_temp) if isinstance(outside_temp, float) else str(outside_temp)
@@ -896,137 +950,197 @@ class TempWebServer:
) )
return html return html
def _build_schedule_form(self, config): def _get_schedule_editor_page(self, sensors, ac_monitor, heater_monitor):
"""Build the schedule editing form.""" """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', []) schedules = config.get('schedules', [])
# Pad with empty schedules up to 4 # Pad with empty schedules up to 4
while len(schedules) < 4: while len(schedules) < 4:
schedules.append({'time': '', 'name': '', 'ac_target': 77.0, 'heater_target': 80.0}) schedules.append({'time': '', 'name': '', 'ac_target': 75.0, 'heater_target': 72.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)
# Build schedule inputs
schedule_inputs = ""
for i, schedule in enumerate(schedules[:4]): for i, schedule in enumerate(schedules[:4]):
form += """ schedule_inputs += """
<div class="schedule-row"> <div style="display: grid; grid-template-columns: 1fr 2fr 1fr 1fr; gap: 10px; margin-bottom: 10px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
<div class="control-group" style="margin: 0;"> <div>
<label class="control-label" style="font-size: 14px;">Time</label> <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}"> <input type="time" name="schedule_{i}_time" value="{time}" style="width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 5px;">
</div> </div>
<div class="control-group" style="margin: 0;"> <div>
<label class="control-label" style="font-size: 14px;">Name</label> <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"> <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>
<div class="control-group" style="margin: 0;"> <div>
<label class="control-label" style="font-size: 14px;">Heater (°F)</label> <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"> <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>
<div class="control-group" style="margin: 0;"> <div>
<label class="control-label" style="font-size: 14px;">AC (°F)</label> <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"> <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>
</div> </div>
""".format( """.format(
i=i, i=i,
time=schedule.get('time', ''), time=schedule.get('time', ''),
name=schedule.get('name', ''), name=schedule.get('name', ''),
ac=schedule.get('ac_target', 77.0), heater=schedule.get('heater_target', 72.0),
heater=schedule.get('heater_target', 80.0) ac=schedule.get('ac_target', 75.0)
) )
form += """ html = """
<div class="control-group" style="margin-top: 20px;"> <!DOCTYPE html>
<button type="submit" name="mode_action" value="save_schedules" class="btn">💾 Save Schedule Changes</button> <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> </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 time
import network import network
import json import json
import gc # ADD THIS - for garbage collection 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) # Initialize pins (LED light onboard)
led = Pin("LED", Pin.OUT) led = Pin("LED", Pin.OUT)
led.low() led.low()
@@ -124,16 +128,20 @@ if wifi and wifi.isconnected():
# Send startup notification to Discord # Send startup notification to Discord
send_discord_message(f"Pico W online at http://{ifconfig[0]}") send_discord_message(f"Pico W online at http://{ifconfig[0]}")
# ===== START: NTP Time Sync ===== # Start web server early so page can load even if time sync is slow
# Sync time with internet time server (required for schedules to work correctly) web_server = TempWebServer(port=80)
# Without this, the Pico's clock starts at 2021 on every reboot web_server.start()
# Attempt time sync non-blocking (short timeout + retry flag)
ntp_synced = False
try: try:
import ntptime import ntptime
ntptime.settime() # Downloads current time from pool.ntp.org ntptime.settime()
ntp_synced = True
print("Time synced with NTP server") print("Time synced with NTP server")
except Exception as e: except Exception as e:
print("Failed to sync time: {}".format(e)) print("Initial NTP sync failed: {}".format(e))
# ===== END: NTP Time Sync ===== # Will retry later in loop
else: else:
# WiFi connection failed # WiFi connection failed
@@ -142,12 +150,6 @@ else:
print("="*50 + "\n") print("="*50 + "\n")
# ===== END: WiFi Connection ===== # ===== 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 ===== # ===== START: Sensor Configuration =====
# Define all temperature sensors and their alert thresholds # Define all temperature sensors and their alert thresholds
SENSOR_CONFIG = { SENSOR_CONFIG = {
@@ -285,21 +287,36 @@ monitors = [
print("Starting monitoring loop...") print("Starting monitoring loop...")
print("Press Ctrl+C to stop\n") 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 ===== # ===== START: Main Loop =====
# Main monitoring loop (runs forever until Ctrl+C) # Main monitoring loop (runs forever until Ctrl+C)
while True: while True:
# Run all monitors (each checks if it's time to run via should_run()) # Run all monitors (each checks if it's time to run via should_run())
run_monitors(monitors) run_monitors(monitors)
# Check for incoming web requests (non-blocking) # Web requests
# Pass schedule_monitor so web interface can reload config when schedules change
web_server.check_requests(sensors, ac_monitor, heater_monitor, schedule_monitor) web_server.check_requests(sensors, ac_monitor, heater_monitor, schedule_monitor)
# ===== START: Garbage Collection ===== # Retry NTP sync every ~10s if not yet synced
# Free up unused memory to prevent fragmentation 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() gc.collect()
# ===== END: Garbage Collection ===== wdt.feed() # Reset watchdog timer (prevent auto-reboot)
# Small delay to prevent CPU overload (0.1 seconds = 10 loops per second)
time.sleep(0.1) time.sleep(0.1)
# ===== END: Main Loop ===== # ===== END: Main Loop =====