Enhance web server to handle schedule updates and configuration loading

This commit is contained in:
Aaron 2025-11-05 21:42:27 -05:00
parent f4be1a7f7d
commit 2817273ba4
2 changed files with 545 additions and 394 deletions

View File

@ -1,5 +1,6 @@
import socket import socket
import time import time
import json
class TempWebServer: class TempWebServer:
"""Simple web server for viewing temperatures and adjusting settings.""" """Simple web server for viewing temperatures and adjusting settings."""
@ -15,12 +16,12 @@ class TempWebServer:
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind(('0.0.0.0', self.port)) self.socket.bind(('0.0.0.0', self.port))
self.socket.listen(1) self.socket.listen(1)
self.socket.setblocking(False) # Non-blocking mode self.socket.setblocking(False)
print(f"Web server started on port {self.port}") print("Web server started on port {}".format(self.port))
except Exception as e: except Exception as e:
print(f"Failed to start web server: {e}") print("Failed to start web server: {}".format(e))
def check_requests(self, sensors, ac_monitor=None, heater_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
@ -32,31 +33,33 @@ class TempWebServer:
# Check if this is a POST request (form submission) # 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) response = self._handle_update(request, sensors, ac_monitor, heater_monitor, schedule_monitor)
elif 'POST /schedule' in request:
response = self._handle_schedule_update(request, sensors, ac_monitor, heater_monitor, schedule_monitor)
else: else:
# Regular GET request # 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:
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('HTTP/1.1 200 OK\r\n')
conn.send('Content-Type: text/html; charset=utf-8\r\n') conn.send('Content-Type: text/html; charset=utf-8\r\n')
conn.send('Connection: close\r\n\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:
pass # No connection, continue pass
except Exception as e: except Exception as e:
print(f"Web server error: {e}") print("Web server error: {}".format(e))
import sys
sys.print_exception(e)
def _save_config_to_file(self, ac_monitor, heater_monitor): def _save_config_to_file(self, config):
"""Save current settings to config.json file.""" """Save configuration to config.json file."""
try: try:
import json
config = {
'ac_target': ac_monitor.target_temp,
'ac_swing': ac_monitor.temp_swing,
'heater_target': heater_monitor.target_temp,
'heater_swing': heater_monitor.temp_swing
}
with open('config.json', 'w') as f: with open('config.json', 'w') as f:
json.dump(config, f) json.dump(config, f)
print("Settings saved to config.json") print("Settings saved to config.json")
@ -65,10 +68,86 @@ class TempWebServer:
print("Error saving config: {}".format(e)) print("Error saving config: {}".format(e))
return False return False
def _handle_update(self, request, sensors, ac_monitor, heater_monitor): def _load_config(self):
"""Load configuration from file."""
try:
with open('config.json', 'r') as f:
return json.load(f)
except:
return {
'ac_target': 77.0,
'ac_swing': 1.0,
'heater_target': 80.0,
'heater_swing': 2.0,
'schedules': [],
'schedule_enabled': False
}
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 = {}
for pair in body.split('&'):
if '=' in pair:
key, value = pair.split('=', 1)
params[key] = value.replace('+', ' ')
# Load current config
config = self._load_config()
# Update schedule enabled status
config['schedule_enabled'] = params.get('schedule_enabled') == 'on'
# Parse schedules
schedules = []
for i in range(4):
time_key = 'schedule_{}_time'.format(i)
name_key = 'schedule_{}_name'.format(i)
ac_key = 'schedule_{}_ac'.format(i)
heater_key = 'schedule_{}_heater'.format(i)
if time_key in params and params[time_key]:
schedule = {
'time': params[time_key],
'name': params.get(name_key, 'Schedule {}'.format(i+1)),
'ac_target': float(params.get(ac_key, 77.0)),
'heater_target': float(params.get(heater_key, 80.0))
}
schedules.append(schedule)
config['schedules'] = schedules
# Save to file
if self._save_config_to_file(config):
print("Schedule settings saved")
# Reload schedule monitor config
if schedule_monitor:
schedule_monitor.reload_config(config)
# Send Discord notification
try:
from scripts.discord_webhook import send_discord_message
status = "enabled" if config['schedule_enabled'] else "disabled"
message = "📅 Schedules updated ({}) - {} schedules configured".format(
status, len(schedules)
)
send_discord_message(message)
except:
pass
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)
def _handle_update(self, request, sensors, ac_monitor, heater_monitor, schedule_monitor):
"""Handle form submission and update settings.""" """Handle form submission and update settings."""
try: try:
# Extract form data from POST body
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 = {}
@ -77,29 +156,37 @@ class TempWebServer:
key, value = pair.split('=', 1) key, value = pair.split('=', 1)
params[key] = float(value) params[key] = float(value)
# Load current config
config = self._load_config()
# Update AC settings # Update AC settings
if 'ac_target' in params and ac_monitor: if 'ac_target' in params and ac_monitor:
ac_monitor.target_temp = params['ac_target'] ac_monitor.target_temp = params['ac_target']
config['ac_target'] = params['ac_target']
print("AC target updated to {}°F".format(params['ac_target'])) print("AC target updated to {}°F".format(params['ac_target']))
if 'ac_swing' in params and ac_monitor: if 'ac_swing' in params and ac_monitor:
ac_monitor.temp_swing = params['ac_swing'] ac_monitor.temp_swing = params['ac_swing']
config['ac_swing'] = params['ac_swing']
print("AC swing updated to {}°F".format(params['ac_swing'])) print("AC swing updated to {}°F".format(params['ac_swing']))
# Update heater settings # Update heater settings
if 'heater_target' in params and heater_monitor: if 'heater_target' in params and heater_monitor:
heater_monitor.target_temp = params['heater_target'] heater_monitor.target_temp = params['heater_target']
config['heater_target'] = params['heater_target']
print("Heater target updated to {}°F".format(params['heater_target'])) print("Heater target updated to {}°F".format(params['heater_target']))
if 'heater_swing' in params and heater_monitor: if 'heater_swing' in params and heater_monitor:
heater_monitor.temp_swing = params['heater_swing'] heater_monitor.temp_swing = params['heater_swing']
config['heater_swing'] = params['heater_swing']
print("Heater swing updated to {}°F".format(params['heater_swing'])) print("Heater swing updated to {}°F".format(params['heater_swing']))
# Save settings to file # Save settings to file
if self._save_config_to_file(ac_monitor, heater_monitor): if self._save_config_to_file(config):
print("Settings persisted to disk") print("Settings persisted to disk")
# Send Discord notification # Send Discord notification
try:
from scripts.discord_webhook import send_discord_message from scripts.discord_webhook import send_discord_message
ac_target_str = str(params.get('ac_target', 'N/A')) ac_target_str = str(params.get('ac_target', 'N/A'))
ac_swing_str = str(params.get('ac_swing', 'N/A')) ac_swing_str = str(params.get('ac_swing', 'N/A'))
@ -110,15 +197,19 @@ class TempWebServer:
ac_target_str, ac_swing_str, heater_target_str, heater_swing_str ac_target_str, ac_swing_str, heater_target_str, heater_swing_str
) )
send_discord_message(message) send_discord_message(message)
except Exception as discord_error:
print("Discord notification failed: {}".format(discord_error))
except Exception as e: except Exception as e:
print("Error updating settings: {}".format(e)) print("Error updating settings: {}".format(e))
import sys
sys.print_exception(e)
# Return updated page
return self._get_status_page(sensors, ac_monitor, heater_monitor, show_success=True) return self._get_status_page(sensors, ac_monitor, heater_monitor, show_success=True)
def _get_status_page(self, sensors, ac_monitor, heater_monitor, show_success=False): def _get_status_page(self, sensors, ac_monitor, heater_monitor, show_success=False):
"""Generate HTML status page.""" """Generate HTML status page."""
try:
# Get current temperatures # Get current temperatures
inside_temps = sensors['inside'].read_all_temps(unit='F') inside_temps = sensors['inside'].read_all_temps(unit='F')
outside_temps = sensors['outside'].read_all_temps(unit='F') outside_temps = sensors['outside'].read_all_temps(unit='F')
@ -137,19 +228,17 @@ class TempWebServer:
current_time[3], current_time[4], current_time[5] current_time[3], current_time[4], current_time[5]
) )
# Load config to show schedules # Load config
try: config = self._load_config()
import json
with open('config.json', 'r') as f:
config = json.load(f)
except:
config = {'schedules': [], 'schedule_enabled': False}
# Build schedule display # Build schedule display
schedule_status = "ENABLED ✅" if config.get('schedule_enabled') else "DISABLED ⚠️" schedule_status = "ENABLED" if config.get('schedule_enabled') else "DISABLED"
schedule_color = "#2ecc71" if config.get('schedule_enabled') else "#95a5a6"
schedule_icon = "" if config.get('schedule_enabled') else "⚠️"
if config.get('schedules'): # Build schedule cards
schedule_cards = "" schedule_cards = ""
if config.get('schedules'):
for schedule in config.get('schedules', []): for schedule in config.get('schedules', []):
schedule_cards += """ schedule_cards += """
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px;"> <div style="background: #f8f9fa; padding: 15px; border-radius: 8px;">
@ -173,6 +262,9 @@ 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">
@ -180,20 +272,20 @@ class TempWebServer:
</div> </div>
""" if show_success else "" """ if show_success else ""
# 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)
html = """ html = """
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>🌱 Auto Garden</title> <title>🌱 Auto Garden</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="refresh" content="30"> <meta http-equiv="refresh" content="30">
<meta charset="utf-8"> <meta charset="utf-8">
<style> <style>
* {{ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{ body {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 1200px; max-width: 1200px;
@ -222,21 +314,9 @@ class TempWebServer:
box-shadow: 0 8px 16px rgba(0,0,0,0.2); box-shadow: 0 8px 16px rgba(0,0,0,0.2);
transition: transform 0.2s; transition: transform 0.2s;
}} }}
.card:hover {{ .card:hover {{ transform: translateY(-5px); }}
transform: translateY(-5px); .card.full-width {{ margin: 15px 0; }}
}} .temp-icon {{ font-size: 64px; text-align: center; margin-bottom: 15px; }}
.card.full-width {{
margin: 15px 0;
}}
.temp-card {{
position: relative;
overflow: hidden;
}}
.temp-icon {{
font-size: 64px;
text-align: center;
margin-bottom: 15px;
}}
.temp-display {{ .temp-display {{
font-size: 56px; font-size: 56px;
font-weight: bold; font-weight: bold;
@ -244,14 +324,8 @@ class TempWebServer:
margin: 15px 0; margin: 15px 0;
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
}} }}
.inside {{ .inside {{ color: #e74c3c; text-shadow: 2px 2px 4px rgba(231, 76, 60, 0.3); }}
color: #e74c3c; .outside {{ color: #3498db; text-shadow: 2px 2px 4px rgba(52, 152, 219, 0.3); }}
text-shadow: 2px 2px 4px rgba(231, 76, 60, 0.3);
}}
.outside {{
color: #3498db;
text-shadow: 2px 2px 4px rgba(52, 152, 219, 0.3);
}}
.label {{ .label {{
font-size: 20px; font-size: 20px;
color: #34495e; color: #34495e;
@ -268,15 +342,8 @@ class TempWebServer:
flex-wrap: wrap; flex-wrap: wrap;
gap: 20px; gap: 20px;
}} }}
.status-item {{ .status-item {{ text-align: center; flex: 1; min-width: 200px; }}
text-align: center; .status-icon {{ font-size: 48px; margin-bottom: 10px; }}
flex: 1;
min-width: 200px;
}}
.status-icon {{
font-size: 48px;
margin-bottom: 10px;
}}
.status-indicator {{ .status-indicator {{
font-size: 22px; font-size: 22px;
font-weight: bold; font-weight: bold;
@ -289,31 +356,21 @@ class TempWebServer:
box-shadow: 0 4px 8px rgba(0,0,0,0.2); box-shadow: 0 4px 8px rgba(0,0,0,0.2);
transition: all 0.3s; transition: all 0.3s;
}} }}
.status-indicator:hover {{ .status-indicator:hover {{ transform: scale(1.05); }}
transform: scale(1.05);
}}
.on {{ .on {{
background: linear-gradient(135deg, #2ecc71, #27ae60); background: linear-gradient(135deg, #2ecc71, #27ae60);
color: white; color: white;
animation: pulse 2s infinite; animation: pulse 2s infinite;
}} }}
.off {{ .off {{ background: linear-gradient(135deg, #95a5a6, #7f8c8d); color: white; }}
background: linear-gradient(135deg, #95a5a6, #7f8c8d); @keyframes pulse {{ 0%, 100% {{ opacity: 1; }} 50% {{ opacity: 0.8; }} }}
color: white;
}}
@keyframes pulse {{
0%, 100% {{ opacity: 1; }}
50% {{ opacity: 0.8; }}
}}
.controls {{ .controls {{
margin-top: 20px; margin-top: 20px;
padding: 20px; padding: 20px;
background: #f8f9fa; background: #f8f9fa;
border-radius: 10px; border-radius: 10px;
}} }}
.control-group {{ .control-group {{ margin: 15px 0; }}
margin: 15px 0;
}}
.control-label {{ .control-label {{
display: block; display: block;
font-size: 16px; font-size: 16px;
@ -321,7 +378,7 @@ class TempWebServer:
color: #34495e; color: #34495e;
margin-bottom: 8px; margin-bottom: 8px;
}} }}
input[type="number"] {{ input[type="number"], input[type="time"], input[type="text"] {{
width: 100%; width: 100%;
padding: 12px; padding: 12px;
font-size: 18px; font-size: 18px;
@ -329,7 +386,7 @@ class TempWebServer:
border-radius: 8px; border-radius: 8px;
transition: border-color 0.3s; transition: border-color 0.3s;
}} }}
input[type="number"]:focus {{ input[type="number"]:focus, input[type="time"]:focus, input[type="text"]:focus {{
outline: none; outline: none;
border-color: #667eea; border-color: #667eea;
}} }}
@ -348,12 +405,8 @@ class TempWebServer:
box-shadow: 0 4px 8px rgba(0,0,0,0.2); box-shadow: 0 4px 8px rgba(0,0,0,0.2);
transition: transform 0.2s; transition: transform 0.2s;
}} }}
.btn:hover {{ .btn:hover {{ transform: translateY(-2px); }}
transform: translateY(-2px); .btn:active {{ transform: translateY(0); }}
}}
.btn:active {{
transform: translateY(0);
}}
.success-message {{ .success-message {{
background: #2ecc71; background: #2ecc71;
color: white; color: white;
@ -364,10 +417,7 @@ class TempWebServer:
margin-bottom: 20px; margin-bottom: 20px;
animation: fadeIn 0.5s; animation: fadeIn 0.5s;
}} }}
@keyframes fadeIn {{ @keyframes fadeIn {{ from {{ opacity: 0; }} to {{ opacity: 1; }} }}
from {{ opacity: 0; }}
to {{ opacity: 1; }}
}}
.footer {{ .footer {{
text-align: center; text-align: center;
color: white; color: white;
@ -382,21 +432,55 @@ class TempWebServer:
margin-top: 12px; margin-top: 12px;
font-weight: 500; font-weight: 500;
}} }}
.degree {{ .degree {{ font-size: 0.6em; vertical-align: super; }}
font-size: 0.6em; .schedule-row {{
vertical-align: super; display: grid;
grid-template-columns: 1fr 2fr 1fr 1fr;
gap: 10px;
margin-bottom: 15px;
padding: 15px;
background: white;
border-radius: 8px;
}} }}
.toggle-switch {{
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}}
.toggle-switch input {{ opacity: 0; width: 0; height: 0; }}
.slider {{
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}}
.slider:before {{
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}}
input:checked + .slider {{ background-color: #2ecc71; }}
input:checked + .slider:before {{ transform: translateX(26px); }}
@media (max-width: 768px) {{ @media (max-width: 768px) {{
.temp-grid {{ .temp-grid {{ grid-template-columns: 1fr; }}
grid-template-columns: 1fr; .status {{ flex-direction: column; }}
}} .schedule-row {{ grid-template-columns: 1fr; }}
.status {{
flex-direction: column;
}}
}} }}
</style> </style>
</head> </head>
<body> <body>
<h1>🌱 Auto Garden Dashboard</h1> <h1>🌱 Auto Garden Dashboard</h1>
{success_message} {success_message}
@ -469,24 +553,25 @@ class TempWebServer:
<div style="text-align: center; margin-bottom: 15px;"> <div style="text-align: center; margin-bottom: 15px;">
<strong>Status:</strong> <strong>Status:</strong>
<span style="color: {schedule_color}; font-weight: bold;"> <span style="color: {schedule_color}; font-weight: bold;">
{schedule_status} {schedule_status} {schedule_icon}
</span> </span>
</div> </div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;"> <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}
</div> </div>
<div class="footer"> <div class="footer">
Last updated: {time}<br> Last updated: {time}<br>
🔄 Auto-refresh every 30 seconds 🔄 Auto-refresh every 30 seconds
</div> </div>
</body> </body>
</html> </html>
""".format( """.format(
success_message=success_html, success_message=success_html,
inside_temp="{:.1f}".format(inside_temp) if isinstance(inside_temp, float) else inside_temp, inside_temp=inside_temp_str,
outside_temp="{:.1f}".format(outside_temp) if isinstance(outside_temp, float) else outside_temp, outside_temp=outside_temp_str,
ac_status=ac_status, ac_status=ac_status,
ac_class="on" if ac_status == "ON" else "off", ac_class="on" if ac_status == "ON" else "off",
heater_status=heater_status, heater_status=heater_status,
@ -497,7 +582,73 @@ class TempWebServer:
heater_swing=heater_monitor.temp_swing if heater_monitor else "N/A", heater_swing=heater_monitor.temp_swing if heater_monitor else "N/A",
time=time_str, time=time_str,
schedule_status=schedule_status, schedule_status=schedule_status,
schedule_color="#2ecc71" if config.get('schedule_enabled') else "#95a5a6", schedule_color=schedule_color,
schedule_cards=schedule_cards schedule_icon=schedule_icon,
schedule_cards=schedule_cards,
schedule_form=schedule_form
) )
return html return html
except Exception as e:
print("Error generating page: {}".format(e))
import sys
sys.print_exception(e)
return "<html><body><h1>Error loading page</h1><pre>{}</pre></body></html>".format(str(e))
def _build_schedule_form(self, config):
"""Build the schedule editing form."""
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})
enabled_checked = 'checked' if config.get('schedule_enabled') else ''
form = """
<form method="POST" action="/schedule" class="controls" style="margin-top: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3 style="color: #34495e; margin: 0;"> Edit Schedules</h3>
<label class="toggle-switch">
<input type="checkbox" name="schedule_enabled" {enabled_checked}>
<span class="slider"></span>
</label>
</div>
""".format(enabled_checked=enabled_checked)
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}">
</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>
<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>
<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>
</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)
)
form += """
<div class="control-group" style="margin-top: 20px;">
<button type="submit" class="btn">💾 Save Schedule</button>
</div>
</form>
"""
return form

View File

@ -198,5 +198,5 @@ print("Press Ctrl+C to stop\n")
# Main monitoring loop # Main monitoring loop
while True: while True:
run_monitors(monitors) run_monitors(monitors)
web_server.check_requests(sensors, ac_monitor, heater_monitor) web_server.check_requests(sensors, ac_monitor, heater_monitor, schedule_monitor)
time.sleep(0.1) time.sleep(0.1)