feat: Implement temporary hold management with config integration and improved notifications

fixes #19
This commit is contained in:
Aaron 2025-11-09 10:11:00 -05:00
parent b6aae121bb
commit 9da21f7c89
3 changed files with 88 additions and 85 deletions

View File

@ -20,7 +20,6 @@ class ScheduleMonitor:
self.last_check = 0 self.last_check = 0
self.current_schedule = None self.current_schedule = None
self.last_applied_schedule = None self.last_applied_schedule = None
self.temp_hold_start_time = None # When temporary hold was activated
self.temp_hold_duration = config.get('temp_hold_duration', 3600) # Use config value, default 1 hour self.temp_hold_duration = config.get('temp_hold_duration', 3600) # Use config value, default 1 hour
def should_run(self): def should_run(self):
@ -137,40 +136,33 @@ class ScheduleMonitor:
# ===== START: Check if temporary hold has expired ===== # ===== START: Check if temporary hold has expired =====
if not self.config.get('schedule_enabled', False) and not self.config.get('permanent_hold', False): if not self.config.get('schedule_enabled', False) and not self.config.get('permanent_hold', False):
# We're in temporary hold mode # In temporary hold mode - check if timer expired
if self.temp_hold_start_time is None: temp_hold_start = self.config.get('temp_hold_start_time') # <-- READ FROM CONFIG NOW
# Just entered hold mode, record start time
self.temp_hold_start_time = time.time() if temp_hold_start is not None:
print("⏸️ Temporary hold started - will auto-resume in {} minutes".format( elapsed = time.time() - temp_hold_start
self.temp_hold_duration // 60
))
else:
# Check if hold has expired
elapsed = time.time() - self.temp_hold_start_time
if elapsed >= self.temp_hold_duration: if elapsed >= self.temp_hold_duration:
# Hold expired, resume schedules # Timer expired - resume automatic scheduling
print("⏰ Temporary hold expired - resuming automatic mode") print("⏰ Temporary hold expired - resuming schedule")
self.config['schedule_enabled'] = True self.config['schedule_enabled'] = True
self.config['permanent_hold'] = False self.config['temp_hold_start_time'] = None
self.temp_hold_start_time = None
# Save updated config # Save updated config
try: try:
import json import json
with open('config.json', 'w') as f: with open('config.json', 'w') as f:
json.dump(self.config, f) json.dump(self.config, f)
except: print("✅ Config updated - automatic mode resumed")
pass except Exception as e:
print("⚠️ Could not save config: {}".format(e))
# Send Discord notification # Notify user
try: try:
from scripts.discord_webhook import send_discord_message from scripts.discord_webhook import send_discord_message
send_discord_message("⏰ Temporary hold expired - Automatic mode resumed") send_discord_message("⏰ Temporary hold expired - Schedule resumed automatically")
except: except:
pass pass
else:
# Not in temporary hold, reset timer
self.temp_hold_start_time = None
# ===== END: Check if temporary hold has expired ===== # ===== END: Check if temporary hold has expired =====
# Find and apply active schedule # Find and apply active schedule

View File

@ -551,44 +551,50 @@ class TempWebServer:
print("Heater swing updated to {}°F".format(params['heater_swing'])) print("Heater swing updated to {}°F".format(params['heater_swing']))
# ===== END: Update Heater Settings ===== # ===== END: Update Heater Settings =====
# ===== START: Disable scheduling ONLY if user manually changed temps ===== # ===== START: ALWAYS enter temporary hold when Save Settings is clicked =====
# Check if this is a user-initiated change (from web form) # User clicked "Save Settings" - enter temporary hold mode
# If AC or heater targets were changed, it's a manual override config['schedule_enabled'] = False
user_changed_temps = ( config['permanent_hold'] = False
('ac_target' in params and params['ac_target'] != config.get('ac_target')) or config['temp_hold_start_time'] = time.time() # SET START TIME
('heater_target' in params and params['heater_target'] != config.get('heater_target')) print("⏸️ Temporary hold activated - Manual override")
)
if user_changed_temps and config.get('schedule_enabled'): # Reload schedule monitor to disable it
config['schedule_enabled'] = False if schedule_monitor:
config['permanent_hold'] = False # Temporary hold schedule_monitor.reload_config(config)
print("⏸️ Schedule disabled - entering TEMPORARY HOLD mode (user override)") # ===== END: ALWAYS enter temporary hold =====
# Reload schedule monitor to disable it
if schedule_monitor:
schedule_monitor.reload_config(config)
# ===== END: Disable scheduling ONLY if user manually changed temps =====
# ===== START: Save settings to file ===== # ===== START: Save settings to file =====
if self._save_config_to_file(config): if self._save_config_to_file(config):
print("Settings persisted to disk") print("Settings persisted to disk")
# ===== RELOAD config into memory immediately =====
try:
with open('config.json', 'r') as f:
updated_config = json.load(f)
# Update the passed-in config dict (updates reference, not copy)
config.clear()
config.update(updated_config)
print("✅ Config reloaded into memory")
except Exception as e:
print("⚠️ Warning: Could not reload config: {}".format(e))
# ===== END: Reload config =====
# ===== END: Save settings to file ===== # ===== END: Save settings to file =====
# ===== START: Send Discord notification ONLY if user changed ===== # ===== START: Send Discord notification =====
if user_changed_temps: try:
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')) heater_target_str = str(params.get('heater_target', 'N/A'))
heater_target_str = str(params.get('heater_target', 'N/A')) heater_swing_str = str(params.get('heater_swing', 'N/A'))
heater_swing_str = str(params.get('heater_swing', 'N/A'))
message = "⏸️ TEMPORARY HOLD - AC: {}F ± {}F | Heater: {}F ± {}F (1 hour)".format(
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
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:
except Exception as discord_error: print("Discord notification failed: {}".format(discord_error))
print("Discord notification failed: {}".format(discord_error))
# ===== END: Send Discord notification ===== # ===== END: Send Discord notification =====
# ===== START: Debug output ===== # ===== START: Debug output =====
@ -706,43 +712,47 @@ class TempWebServer:
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)
# ===== START: Add HOLD mode banner with countdown timer ===== # ===== START: Add HOLD mode banner with countdown timer =====
hold_banner = "" hold_banner = ""
# Calculate remaining time for temporary hold # Calculate remaining time for temporary hold
temp_hold_remaining = "" temp_hold_remaining = ""
if not config.get('schedule_enabled', False) and not config.get('permanent_hold', False): if not config.get('schedule_enabled', False) and not config.get('permanent_hold', False):
# In temporary hold - check if we have schedule_monitor with timer # In temporary hold - check timer from CONFIG (not schedule_monitor)
if schedule_monitor and hasattr(schedule_monitor, 'temp_hold_start_time'): temp_hold_start = config.get('temp_hold_start_time') # READ FROM CONFIG
if schedule_monitor.temp_hold_start_time is not None:
# Calculate elapsed time if temp_hold_start is not None:
elapsed = time.time() - schedule_monitor.temp_hold_start_time # Get hold duration from config
# Calculate remaining time temp_hold_duration = config.get('temp_hold_duration', 3600)
remaining = schedule_monitor.temp_hold_duration - elapsed
# Calculate elapsed time
elapsed = time.time() - temp_hold_start
# Calculate remaining time
remaining = temp_hold_duration - elapsed
if remaining > 0:
# Convert to minutes
mins_remaining = int(remaining // 60)
if remaining > 0: # Format the display text
# Convert to minutes if mins_remaining > 60:
mins_remaining = int(remaining // 60) # Show hours and minutes for long durations
hours = mins_remaining // 60
# Format the display text mins = mins_remaining % 60
if mins_remaining > 60: temp_hold_remaining = " - {}h {}m remaining".format(hours, mins)
# Show hours and minutes for long durations elif mins_remaining > 1:
hours = mins_remaining // 60 # Show just minutes
mins = mins_remaining % 60 temp_hold_remaining = " - {} min remaining".format(mins_remaining)
temp_hold_remaining = " - {}h {}m remaining".format(hours, mins) elif mins_remaining == 1:
elif mins_remaining > 1: # Show singular "minute"
# Show just minutes temp_hold_remaining = " - 1 minute remaining"
temp_hold_remaining = " - {} min remaining".format(mins_remaining)
elif mins_remaining == 1:
# Show singular "minute"
temp_hold_remaining = " - 1 minute remaining"
else:
# Less than 1 minute left
secs_remaining = int(remaining)
temp_hold_remaining = " - {}s remaining".format(secs_remaining)
else: else:
# Timer expired (should auto-resume soon) # Less than 1 minute left
temp_hold_remaining = " - Resuming..." secs_remaining = int(remaining)
temp_hold_remaining = " - {}s remaining".format(secs_remaining)
else:
# Timer expired (should auto-resume soon)
temp_hold_remaining = " - Resuming..."
if config.get('permanent_hold', False): if config.get('permanent_hold', False):
# PERMANENT HOLD - No timer, stays until user resumes or reboot # PERMANENT HOLD - No timer, stays until user resumes or reboot
@ -752,8 +762,7 @@ class TempWebServer:
</div> </div>
""" """
elif not config.get('schedule_enabled', False) and has_schedules: elif not config.get('schedule_enabled', False) and has_schedules:
# TEMPORARY HOLD - Show countdown timer if available # TEMPORARY HOLD - Show countdown timer
# Note: We'll need to accept schedule_monitor as parameter to access timer
hold_banner = """ hold_banner = """
<div style="background: linear-gradient(135deg, #f39c12, #e67e22); color: white; padding: 15px; border-radius: 10px; text-align: center; font-weight: bold; margin-bottom: 20px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); animation: fadeIn 0.5s;"> <div style="background: linear-gradient(135deg, #f39c12, #e67e22); color: white; padding: 15px; border-radius: 10px; text-align: center; font-weight: bold; margin-bottom: 20px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); animation: fadeIn 0.5s;">
TEMPORARY HOLD - Manual override active{remaining} TEMPORARY HOLD - Manual override active{remaining}

View File

@ -106,6 +106,8 @@ if 'schedule_enabled' in config:
config['schedule_enabled'] = True # Always enable schedules on boot config['schedule_enabled'] = True # Always enable schedules on boot
if 'permanent_hold' in config: if 'permanent_hold' in config:
config['permanent_hold'] = False # Always clear permanent hold on boot config['permanent_hold'] = False # Always clear permanent hold on boot
if 'temp_hold_start_time' in config:
config['temp_hold_start_time'] = None # Clear temp hold start time
# Save the reset config immediately # Save the reset config immediately
try: try: