import socket import time import json class TempWebServer: """Simple web server for viewing temperatures and adjusting settings.""" def __init__(self, port=80): self.port = port self.socket = None self.sensors = {} def start(self): """Start the web server (non-blocking).""" try: self.socket = socket.socket() self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind(('0.0.0.0', self.port)) self.socket.listen(1) self.socket.setblocking(False) 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 'POST /schedule' in request: response = self._handle_schedule_update(request, sensors, ac_monitor, heater_monitor, schedule_monitor) 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.sendall(response.encode('utf-8')) conn.close() except OSError: pass except Exception as e: 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.""" try: with open('config.json', 'w') as f: json.dump(config, f) print("Settings saved to config.json") return True except Exception as e: print("Error saving config: {}".format(e)) return False 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, 'permanent_hold': 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() # ===== START: Handle mode actions ===== mode_action = params.get('mode_action', '') if mode_action == 'resume': # Resume automatic scheduling config['schedule_enabled'] = True config['permanent_hold'] = False if self._save_config_to_file(config): print("▶️ Schedule resumed - Automatic mode") if schedule_monitor: schedule_monitor.reload_config(config) try: from scripts.discord_webhook import send_discord_message send_discord_message("▶️ Schedule resumed - Automatic temperature control active") except: pass return self._get_status_page(sensors, ac_monitor, heater_monitor, show_success=True) elif mode_action == 'temporary_hold': # Enter temporary hold (pause schedules temporarily) config['schedule_enabled'] = False config['permanent_hold'] = False if self._save_config_to_file(config): print("⏸️ Temporary hold activated") if schedule_monitor: schedule_monitor.reload_config(config) try: from scripts.discord_webhook import send_discord_message send_discord_message("⏸️ Temporary hold - Schedules paused, manual control active") except: pass return self._get_status_page(sensors, ac_monitor, heater_monitor, show_success=True) elif mode_action == 'permanent_hold': # Enter permanent hold (disable schedules permanently) config['schedule_enabled'] = False config['permanent_hold'] = True if self._save_config_to_file(config): print("🛑 Permanent hold activated") if schedule_monitor: schedule_monitor.reload_config(config) try: from scripts.discord_webhook import send_discord_message send_discord_message("🛑 Permanent hold - Schedules disabled, manual control only") except: pass return self._get_status_page(sensors, ac_monitor, heater_monitor, show_success=True) # ===== END: Handle mode actions ===== elif mode_action == 'save_schedules': # Just fall through to schedule parsing below pass # ===== END: Handle mode actions ===== # ===== START: Handle schedule configuration save ===== # Parse schedules from form 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 configuration saved") if schedule_monitor: schedule_monitor.reload_config(config) # Send Discord notification try: from scripts.discord_webhook import send_discord_message mode = "automatic" if config.get('schedule_enabled') else "hold" message = "📅 Schedules updated ({} mode) - {} schedules configured".format( mode, len(schedules) ) send_discord_message(message) except: pass # ===== END: Handle schedule configuration save ===== 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.""" 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] = float(value) # Load current config config = self._load_config() # ===== START: Update AC Settings ===== if 'ac_target' in params and ac_monitor: ac_monitor.target_temp = params['ac_target'] # Update monitor config['ac_target'] = params['ac_target'] # Update config print("AC target updated to {}°F".format(params['ac_target'])) if 'ac_swing' in params and ac_monitor: ac_monitor.temp_swing = params['ac_swing'] # Update monitor config['ac_swing'] = params['ac_swing'] # Update config print("AC swing updated to {}°F".format(params['ac_swing'])) # ===== END: Update AC Settings ===== # ===== START: Update Heater Settings ===== if 'heater_target' in params and heater_monitor: heater_monitor.target_temp = params['heater_target'] # Update monitor config['heater_target'] = params['heater_target'] # Update config print("Heater target updated to {}°F".format(params['heater_target'])) if 'heater_swing' in params and heater_monitor: heater_monitor.temp_swing = params['heater_swing'] # Update monitor config['heater_swing'] = params['heater_swing'] # Update config print("Heater swing updated to {}°F".format(params['heater_swing'])) # ===== END: Update Heater Settings ===== # ===== START: Disable scheduling and enter HOLD mode ===== if config.get('schedule_enabled'): config['schedule_enabled'] = False config['permanent_hold'] = False # ✅ ADD THIS - ensure it's temporary print("⏸️ Schedule disabled - entering TEMPORARY HOLD mode") # Reload schedule monitor to disable it if schedule_monitor: schedule_monitor.reload_config(config) # ===== END: Disable scheduling and enter HOLD mode ===== # ===== START: Save settings to file ===== if self._save_config_to_file(config): print("Settings persisted to disk") # ===== END: Save settings to file ===== # ===== START: Send Discord notification ===== try: from scripts.discord_webhook import send_discord_message ac_target_str = str(params.get('ac_target', 'N/A')) ac_swing_str = str(params.get('ac_swing', 'N/A')) heater_target_str = str(params.get('heater_target', 'N/A')) heater_swing_str = str(params.get('heater_swing', 'N/A')) message = "⏸️ HOLD Mode - Manual override: AC: {}F +/- {}F | Heater: {}F +/- {}F (Schedule disabled)".format( ac_target_str, ac_swing_str, heater_target_str, heater_swing_str ) send_discord_message(message) except Exception as discord_error: print("Discord notification failed: {}".format(discord_error)) # ===== END: Send Discord notification ===== # ===== START: Debug output ===== print("DEBUG: After update, monitor values are:") if ac_monitor: print(" AC target: {}".format(ac_monitor.target_temp)) print(" AC swing: {}".format(ac_monitor.temp_swing)) if heater_monitor: print(" Heater target: {}".format(heater_monitor.target_temp)) print(" Heater swing: {}".format(heater_monitor.temp_swing)) # ===== END: Debug output ===== except Exception as e: print("Error updating settings: {}".format(e)) import sys sys.print_exception(e) 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): """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') 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 AC/Heater status ac_status = "ON" if ac_monitor and ac_monitor.ac.get_state() else "OFF" heater_status = "ON" if heater_monitor and heater_monitor.heater.get_state() else "OFF" # Get current time current_time = time.localtime() time_str = "{}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format( current_time[0], current_time[1], current_time[2], current_time[3], current_time[4], current_time[5] ) # Load config config = self._load_config() # ===== START: Determine schedule status display ===== has_schedules = len([s for s in config.get('schedules', []) if s.get('time')]) > 0 if not has_schedules: schedule_status = "NO SCHEDULES" schedule_color = "#95a5a6" schedule_icon = "⚠️" elif config.get('schedule_enabled'): schedule_status = "AUTOMATIC" schedule_color = "#2ecc71" schedule_icon = "✅" elif config.get('permanent_hold', False): schedule_status = "PERMANENT HOLD" schedule_color = "#e74c3c" schedule_icon = "🛑" else: schedule_status = "TEMPORARY HOLD" schedule_color = "#f39c12" schedule_icon = "⏸️" # ===== END: Determine schedule status display ===== # Build schedule cards schedule_cards = "" if config.get('schedules'): for schedule in config.get('schedules', []): # ===== START: Decode URL-encoded values ===== # Replace %3A with : and + with space time_value = schedule.get('time', 'N/A').replace('%3A', ':') name_value = schedule.get('name', 'Unnamed').replace('+', ' ') # ===== END: Decode URL-encoded values ===== schedule_cards += """
🕐 {time} - {name}
AC: {ac_temp}°F | Heater: {heater_temp}°F
""".format( time=time_value, # Use decoded value name=name_value, # Use decoded value ac_temp=schedule.get('ac_target', 'N/A'), heater_temp=schedule.get('heater_target', 'N/A') ) else: schedule_cards = """
No schedules configured
""" # Build schedule form schedule_form = self._build_schedule_form(config) # Success message success_html = """
✅ Settings updated successfully!
""" 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) # ===== START: Add HOLD mode banner ===== hold_banner = "" if config.get('permanent_hold', False): hold_banner = """
🛑 PERMANENT HOLD - Schedules disabled (Manual control only)
""" elif not config.get('schedule_enabled', False) and has_schedules: hold_banner = """
⏸️ TEMPORARY HOLD - Manual override active (Schedule paused)
""" # ===== END: Add HOLD mode banner ===== html = """ 🌱 Auto Garden

🌱 Auto Garden Dashboard

{hold_banner} {success_message}
🏠
Indoor Climate
{inside_temp}°F
🌤️
Outdoor Climate
{outside_temp}°F
❄️
Air Conditioning
{ac_status}
Target: {ac_target}°F ± {ac_swing}°F
🔥
Heating System
{heater_status}
Target: {heater_target}°F ± {heater_swing}°F

⚙️ Adjust Settings

📅 Daily Schedule

Status: {schedule_status} {schedule_icon}
{schedule_cards}
{schedule_form}
""".format( hold_banner=hold_banner, success_message=success_html, inside_temp=inside_temp_str, outside_temp=outside_temp_str, ac_status=ac_status, ac_class="on" if ac_status == "ON" else "off", heater_status=heater_status, heater_class="on" if heater_status == "ON" else "off", ac_target=ac_monitor.target_temp if ac_monitor else "N/A", ac_swing=ac_monitor.temp_swing if ac_monitor else "N/A", heater_target=heater_monitor.target_temp if heater_monitor else "N/A", heater_swing=heater_monitor.temp_swing if heater_monitor else "N/A", time=time_str, schedule_status=schedule_status, schedule_color=schedule_color, schedule_icon=schedule_icon, schedule_cards=schedule_cards, schedule_form=schedule_form ) return html except Exception as e: print("Error generating page: {}".format(e)) import sys sys.print_exception(e) return "

Error loading page

{}
".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}) # ===== 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 = """
ℹ️ Configure schedules below, then choose a mode
""" elif current_mode == "automatic": # Automatic mode active mode_buttons = """
✅ Automatic Mode Active
Temperatures automatically adjust based on schedule
""" elif current_mode == "temporary_hold": # Temporary hold (manual override) mode_buttons = """
⏸️ Temporary Hold Active
Manual settings in use - Schedule paused
""" else: # permanent_hold # Permanent hold (schedules disabled by user) mode_buttons = """
🛑 Permanent Hold Active
Schedules disabled - Manual control only
""" # ===== END: Build mode control buttons ===== form = """

⚙️ Schedule Configuration

{mode_buttons} """.format(mode_buttons=mode_buttons) for i, schedule in enumerate(schedules[:4]): form += """
""".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 += """
""" return form