Sfoglia il codice sorgente

Fix tz and turn off wled in still sands (#78)

* fix performance issue

* fix async calls

* move motion control to its own thread

* imporve browse page perfomance

* fix cache

* further optimize page load

* all endpoints should run async

* fix syntax

* motion should use a different core

* fix async issue

* optimize settings page loading

* fix folder loading

* Add still sand feature

* Add option to turn off wled when in Still Sands

* get timezone from system

* fix docker tz issue

* fix editing still sands

* add status

* Bump version

---------

Co-authored-by: Claude <noreply@anthropic.com>
Tuan Nguyen 4 mesi fa
parent
commit
97f28820c4
7 ha cambiato i file con 160 aggiunte e 17 eliminazioni
  1. 1 1
      VERSION
  2. 3 1
      docker-compose.yml
  3. 5 1
      main.py
  4. 72 3
      modules/core/pattern_manager.py
  5. 3 0
      modules/core/state.py
  6. 43 11
      static/js/settings.js
  7. 33 0
      templates/settings.html

+ 1 - 1
VERSION

@@ -1 +1 @@
-3.3.1
+3.3.2

+ 3 - 1
docker-compose.yml

@@ -1,12 +1,14 @@
 services:
   dune-weaver:
-    build: . # Uncomment this if you need to build 
+    build: . # Uncomment this if you need to build
     image: ghcr.io/tuanchris/dune-weaver:main # Use latest production image
     restart: always
     ports:
       - "8080:8080" # Map port 8080 of the container to 8080 of the host (access via http://localhost:8080)
     volumes:
       - .:/app
+      # Mount timezone file from host for Still Sands scheduling
+      - /etc/timezone:/etc/host-timezone:ro
     devices:
       - "/dev/ttyACM0:/dev/ttyACM0"
     privileged: true

+ 5 - 1
main.py

@@ -162,6 +162,7 @@ class TimeSlot(BaseModel):
 
 class ScheduledPauseRequest(BaseModel):
     enabled: bool
+    control_wled: Optional[bool] = False
     time_slots: List[TimeSlot] = []
 
 class CoordinateRequest(BaseModel):
@@ -320,6 +321,7 @@ async def get_scheduled_pause():
     """Get current Still Sands settings."""
     return {
         "enabled": state.scheduled_pause_enabled,
+        "control_wled": state.scheduled_pause_control_wled,
         "time_slots": state.scheduled_pause_time_slots
     }
 
@@ -364,10 +366,12 @@ async def set_scheduled_pause(request: ScheduledPauseRequest):
 
         # Update state
         state.scheduled_pause_enabled = request.enabled
+        state.scheduled_pause_control_wled = request.control_wled
         state.scheduled_pause_time_slots = [slot.model_dump() for slot in request.time_slots]
         state.save()
 
-        logger.info(f"Still Sands {'enabled' if request.enabled else 'disabled'} with {len(request.time_slots)} time slots")
+        wled_msg = " (with WLED control)" if request.control_wled else ""
+        logger.info(f"Still Sands {'enabled' if request.enabled else 'disabled'} with {len(request.time_slots)} time slots{wled_msg}")
         return {"success": True, "message": "Still Sands settings updated"}
 
     except HTTPException:

+ 72 - 3
modules/core/pattern_manager.py

@@ -1,4 +1,5 @@
 import os
+from zoneinfo import ZoneInfo
 import threading
 import time
 import random
@@ -32,12 +33,67 @@ pattern_lock = asyncio.Lock()
 # Progress update task
 progress_update_task = None
 
+# Cache timezone at module level - read once per session
+_cached_timezone = None
+_cached_zoneinfo = None
+
+def _get_system_timezone():
+    """Get and cache the system timezone. Called once per session."""
+    global _cached_timezone, _cached_zoneinfo
+
+    if _cached_timezone is not None:
+        return _cached_zoneinfo
+
+    user_tz = 'UTC'  # Default fallback
+
+    # Try to read timezone from /etc/host-timezone (mounted from host)
+    try:
+        if os.path.exists('/etc/host-timezone'):
+            with open('/etc/host-timezone', 'r') as f:
+                user_tz = f.read().strip()
+                logger.info(f"Still Sands using timezone: {user_tz} (from host system)")
+        # Fallback to /etc/timezone if host-timezone doesn't exist
+        elif os.path.exists('/etc/timezone'):
+            with open('/etc/timezone', 'r') as f:
+                user_tz = f.read().strip()
+                logger.info(f"Still Sands using timezone: {user_tz} (from container)")
+        # Fallback to TZ environment variable
+        elif os.environ.get('TZ'):
+            user_tz = os.environ.get('TZ')
+            logger.info(f"Still Sands using timezone: {user_tz} (from environment)")
+        else:
+            logger.info("Still Sands using timezone: UTC (default)")
+    except Exception as e:
+        logger.debug(f"Could not read timezone: {e}")
+
+    # Cache the timezone
+    _cached_timezone = user_tz
+    try:
+        _cached_zoneinfo = ZoneInfo(user_tz)
+    except Exception as e:
+        logger.warning(f"Invalid timezone '{user_tz}', falling back to system time: {e}")
+        _cached_zoneinfo = None
+
+    return _cached_zoneinfo
+
 def is_in_scheduled_pause_period():
     """Check if current time falls within any scheduled pause period."""
     if not state.scheduled_pause_enabled or not state.scheduled_pause_time_slots:
         return False
 
-    now = datetime.now()
+    # Get cached timezone
+    tz_info = _get_system_timezone()
+
+    try:
+        # Get current time in user's timezone
+        if tz_info:
+            now = datetime.now(tz_info)
+        else:
+            now = datetime.now()
+    except Exception as e:
+        logger.warning(f"Error getting current time: {e}")
+        now = datetime.now()
+
     current_time = now.time()
     current_weekday = now.strftime("%A").lower()  # monday, tuesday, etc.
 
@@ -588,10 +644,19 @@ async def run_theta_rho_file(file_path, is_playlist=False):
                         logger.info("Execution paused (manual)...")
                     else:
                         logger.info("Execution paused (scheduled pause period)...")
-
-                    if state.led_controller:
+                        # Turn off WLED if scheduled pause and control_wled is enabled
+                        if state.scheduled_pause_control_wled and state.led_controller:
+                            logger.info("Turning off WLED lights during Still Sands period")
+                            state.led_controller.set_power(0)
+
+                    # Only show idle effect if NOT in scheduled pause with WLED control
+                    # (manual pause always shows idle effect)
+                    if state.led_controller and not (scheduled_pause and state.scheduled_pause_control_wled):
                         effect_idle(state.led_controller)
 
+                    # Remember if we turned off WLED for scheduled pause
+                    wled_was_off_for_scheduled = scheduled_pause and state.scheduled_pause_control_wled and not manual_pause
+
                     # Wait until both manual pause is released AND we're outside scheduled pause period
                     while state.pause_requested or is_in_scheduled_pause_period():
                         await asyncio.sleep(1)  # Check every second
@@ -601,6 +666,10 @@ async def run_theta_rho_file(file_path, is_playlist=False):
 
                     logger.info("Execution resumed...")
                     if state.led_controller:
+                        # Turn WLED back on if it was turned off for scheduled pause
+                        if wled_was_off_for_scheduled:
+                            logger.info("Turning WLED lights back on as Still Sands period ended")
+                            state.led_controller.set_power(1)
                         effect_playing(state.led_controller)
 
                 # Dynamically determine the speed for each movement

+ 3 - 0
modules/core/state.py

@@ -64,6 +64,7 @@ class AppState:
         # Still Sands settings
         self.scheduled_pause_enabled = False
         self.scheduled_pause_time_slots = []  # List of time slot dictionaries
+        self.scheduled_pause_control_wled = False  # Turn off WLED during pause periods
         
         self.load()
 
@@ -198,6 +199,7 @@ class AppState:
             "auto_play_shuffle": self.auto_play_shuffle,
             "scheduled_pause_enabled": self.scheduled_pause_enabled,
             "scheduled_pause_time_slots": self.scheduled_pause_time_slots,
+            "scheduled_pause_control_wled": self.scheduled_pause_control_wled,
         }
 
     def from_dict(self, data):
@@ -236,6 +238,7 @@ class AppState:
         self.auto_play_shuffle = data.get("auto_play_shuffle", False)
         self.scheduled_pause_enabled = data.get("scheduled_pause_enabled", False)
         self.scheduled_pause_time_slots = data.get("scheduled_pause_time_slots", [])
+        self.scheduled_pause_control_wled = data.get("scheduled_pause_control_wled", False)
 
     def save(self):
         """Save the current state to a JSON file."""

+ 43 - 11
static/js/settings.js

@@ -1038,6 +1038,7 @@ async function initializeStillSandsMode() {
     const addTimeSlotButton = document.getElementById('addTimeSlotButton');
     const saveStillSandsButton = document.getElementById('savePauseSettings');
     const timeSlotsContainer = document.getElementById('timeSlotsContainer');
+    const wledControlToggle = document.getElementById('stillSandsWledControl');
 
     // Check if elements exist
     if (!stillSandsToggle || !stillSandsSettings || !addTimeSlotButton || !saveStillSandsButton || !timeSlotsContainer) {
@@ -1071,8 +1072,22 @@ async function initializeStillSandsMode() {
             stillSandsSettings.style.display = 'block';
         }
 
+        // Load WLED control setting
+        if (wledControlToggle) {
+            wledControlToggle.checked = data.control_wled || false;
+        }
+
         // Load existing time slots
         timeSlots = data.time_slots || [];
+
+        // Assign IDs to loaded slots BEFORE rendering
+        if (timeSlots.length > 0) {
+            slotIdCounter = 0;
+            timeSlots.forEach(slot => {
+                slot.id = ++slotIdCounter;
+            });
+        }
+
         renderTimeSlots();
     } catch (error) {
         logMessage(`Error loading Still Sands settings: ${error.message}`, LOG_TYPE.ERROR);
@@ -1274,12 +1289,18 @@ async function initializeStillSandsMode() {
             return;
         }
 
+        // Update button UI to show loading state
+        const originalButtonHTML = saveStillSandsButton.innerHTML;
+        saveStillSandsButton.disabled = true;
+        saveStillSandsButton.innerHTML = '<span class="material-icons text-lg animate-spin">refresh</span><span class="truncate">Saving...</span>';
+
         try {
             const response = await fetch('/api/scheduled-pause', {
                 method: 'POST',
                 headers: { 'Content-Type': 'application/json' },
                 body: JSON.stringify({
                     enabled: stillSandsToggle.checked,
+                    control_wled: wledControlToggle ? wledControlToggle.checked : false,
                     time_slots: timeSlots.map(slot => ({
                         start_time: slot.start_time,
                         end_time: slot.end_time,
@@ -1294,24 +1315,26 @@ async function initializeStillSandsMode() {
                 throw new Error(errorData.detail || 'Failed to save Still Sands settings');
             }
 
+            // Show success state temporarily
+            saveStillSandsButton.innerHTML = '<span class="material-icons text-lg">check</span><span class="truncate">Saved!</span>';
             showStatusMessage('Still Sands settings saved successfully', 'success');
+
+            // Restore button after 2 seconds
+            setTimeout(() => {
+                saveStillSandsButton.innerHTML = originalButtonHTML;
+                saveStillSandsButton.disabled = false;
+            }, 2000);
         } catch (error) {
             logMessage(`Error saving Still Sands settings: ${error.message}`, LOG_TYPE.ERROR);
             showStatusMessage(`Failed to save settings: ${error.message}`, 'error');
-        }
-    }
 
-    // Initialize slot ID counter
-    if (timeSlots.length > 0) {
-        slotIdCounter = Math.max(...timeSlots.map(slot => slot.id || 0));
+            // Restore button immediately on error
+            saveStillSandsButton.innerHTML = originalButtonHTML;
+            saveStillSandsButton.disabled = false;
+        }
     }
 
-    // Assign IDs to existing slots if they don't have them
-    timeSlots.forEach(slot => {
-        if (!slot.id) {
-            slot.id = ++slotIdCounter;
-        }
-    });
+    // Note: Slot IDs are now assigned during initialization above, before first render
 
     // Event listeners
     stillSandsToggle.addEventListener('change', async () => {
@@ -1332,4 +1355,13 @@ async function initializeStillSandsMode() {
 
     addTimeSlotButton.addEventListener('click', addTimeSlot);
     saveStillSandsButton.addEventListener('click', saveStillSandsSettings);
+
+    // Add listener for WLED control toggle
+    if (wledControlToggle) {
+        wledControlToggle.addEventListener('change', async () => {
+            logMessage(`WLED control toggle changed: ${wledControlToggle.checked}`, LOG_TYPE.INFO);
+            // Auto-save when WLED control changes
+            await saveStillSandsSettings();
+        });
+    }
 }

+ 33 - 0
templates/settings.html

@@ -178,6 +178,20 @@ input:checked + .slider:before {
   background-color: #0c7ff2;
 }
 
+/* Spin animation for loading states */
+@keyframes spin {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+.animate-spin {
+  animation: spin 1s linear infinite;
+}
+
 /* Time slot specific styles */
 .time-slot-item {
   background-color: #f8fafc;
@@ -617,6 +631,25 @@ input:checked + .slider:before {
       </div>
 
       <div id="scheduledPauseSettings" class="space-y-4" style="display: none;">
+        <!-- WLED Control Option -->
+        <div class="bg-amber-50 rounded-lg p-4 border border-amber-200">
+          <div class="flex items-center justify-between">
+            <div class="flex-1">
+              <h4 class="text-slate-800 text-sm font-medium flex items-center gap-2">
+                <span class="material-icons text-amber-600 text-base">lightbulb</span>
+                Control WLED Lights
+              </h4>
+              <p class="text-xs text-slate-600 mt-1">
+                Turn off WLED lights during still periods for complete rest
+              </p>
+            </div>
+            <label class="switch">
+              <input type="checkbox" id="stillSandsWledControl">
+              <span class="slider round"></span>
+            </label>
+          </div>
+        </div>
+
         <div class="bg-slate-50 rounded-lg p-4 space-y-4">
           <div class="flex items-center justify-between">
             <h4 class="text-slate-800 text-base font-semibold">Still Periods</h4>