From 6fd172ce2e863f675e73c5a2ac1a80de5e53d5dc Mon Sep 17 00:00:00 2001 From: Kalzu Rekku Date: Sun, 26 Jan 2025 22:02:23 +0200 Subject: [PATCH] Active alarms almost work. --- alert_api/alarm_api.py | 11 ---- alert_api/alarm_siren.py | 99 +++++++++++++++++++++++------------- alert_api/data_classes.py | 16 ++---- alert_api/main.py | 2 +- alert_api/ncurses_ui.py | 83 +++++++++++++++++++++++++++++- alert_api/ncurses_ui_draw.py | 26 ++++++++++ 6 files changed, 174 insertions(+), 63 deletions(-) diff --git a/alert_api/alarm_api.py b/alert_api/alarm_api.py index bfd78f7..8754e47 100644 --- a/alert_api/alarm_api.py +++ b/alert_api/alarm_api.py @@ -134,17 +134,6 @@ class AlertApi(BaseHTTPRequestHandler): def run(server_class=HTTPServer, handler_class=AlertApi, port=8000): - # Set up logging configuration - logging.basicConfig( - level=logging.DEBUG, # Set to DEBUG to show all log levels - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.StreamHandler(), # Console handler - logging.FileHandler('alert_api.log') # File handler - ] - ) - - logger = logging.getLogger('AlertApi') logger.info(f"Starting AlertApi on port {port}") server_address = ("", port) diff --git a/alert_api/alarm_siren.py b/alert_api/alarm_siren.py index d6dce53..5c5ee8f 100644 --- a/alert_api/alarm_siren.py +++ b/alert_api/alarm_siren.py @@ -100,12 +100,11 @@ class AlarmSiren: return None def _play_audio(self, file_path: str, volume: int = 100): - """Play audio file using mpg123""" + """Play audio file using mpg123 in the background.""" try: - # Ensure the file exists if not os.path.exists(file_path): logger.error(f"Audio file not found: {file_path}") - return False + return None # Construct mpg123 command with volume control volume_adjust = f"-g {volume}" @@ -113,12 +112,12 @@ class AlarmSiren: logger.info(f"Playing alarm: {file_path}") - # Track the process for potential interruption - process = subprocess.Popen(cmd) + # Run mpg123 in the background, suppressing stdout/stderr + process = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return process except Exception as e: logger.error(f"Error playing audio: {e}") - return False + return None def _playback_worker(self): """Background thread for managing alarm playback""" @@ -129,10 +128,12 @@ class AlarmSiren: new_alarm = self.alarm_queue.get(timeout=1) alarm_time = self._calculate_next_alarm_time(new_alarm) if alarm_time: - self.active_alarms[new_alarm.get('id', id(new_alarm))] = { + alarm_id = new_alarm.get('id', id(new_alarm)) + self.active_alarms[alarm_id] = { 'config': new_alarm, 'trigger_time': alarm_time, - 'snooze_count': 0 + 'snooze_count': 0, + 'process': None } except queue.Empty: pass @@ -140,7 +141,11 @@ class AlarmSiren: # Check for control signals (snooze/dismiss) try: control_msg = self.control_queue.get(timeout=0.1) - # Handle control message logic + alarm_id = control_msg.get('alarm_id') + if control_msg['type'] == 'snooze': + self.snooze_alarm(alarm_id) + elif control_msg['type'] == 'dismiss': + self.dismiss_alarm(alarm_id) except queue.Empty: pass @@ -148,23 +153,30 @@ class AlarmSiren: now = datetime.now() for alarm_id, alarm_info in list(self.active_alarms.items()): if now >= alarm_info['trigger_time']: - # Trigger alarm - process = self._play_audio( - alarm_info['config']['file_to_play'], - alarm_info['config'].get('metadata', {}).get('volume', 100) - ) + # Trigger alarm if not already active + if alarm_info['process'] is None: + alarm_info['process'] = self._play_audio( + alarm_info['config']['file_to_play'], + alarm_info['config'].get('metadata', {}).get('volume', 100) + ) + logger.info(f"Alarm {alarm_id} triggered at {now}.") - # Handle repeat and snooze logic - if process: - # Wait for user interaction or timeout - # In a real implementation, this would be more sophisticated - time.sleep(30) # Placeholder for user interaction + # Notify UI about the triggered alarm + self.control_queue.put({ + 'type': 'trigger', + 'alarm_id': alarm_id, + 'info': alarm_info + }) - # Determine next trigger based on repeat rule + # Handle alarms that have been snoozed or dismissed + if alarm_info['process'] and alarm_info['process'].poll() is not None: + # Process has finished naturally next_trigger = self._calculate_next_alarm_time(alarm_info['config']) if next_trigger: alarm_info['trigger_time'] = next_trigger + alarm_info['process'] = None else: + logger.info(f"Removing non-repeating alarm {alarm_id}.") del self.active_alarms[alarm_id] time.sleep(1) # Prevent tight loop @@ -183,33 +195,48 @@ class AlarmSiren: # Default snooze configuration if not provided snooze_config = alarm_config.get('snooze', { - 'enabled': True, + 'enabled': True, 'duration': 5, # Default 5 minutes 'max_count': 3 # Default max 3 snoozes }) - # Ensure all required keys exist with default values - snooze_config.setdefault('enabled', True) - snooze_config.setdefault('duration', 5) - snooze_config.setdefault('max_count', 3) - - # Check if snoozing is allowed - if not snooze_config['enabled']: + if not snooze_config.get('enabled', True): logger.warning(f"Snooze not enabled for alarm {alarm_id}") return False # Check snooze count - current_snooze_count = alarm_info.get('snooze_count', 0) - if current_snooze_count >= snooze_config['max_count']: + if alarm_info.get('snooze_count', 0) >= snooze_config['max_count']: logger.warning(f"Maximum snooze count reached for alarm {alarm_id}") return False - # Increment snooze count - alarm_info['snooze_count'] = current_snooze_count + 1 - - # Set next trigger time - snooze_duration = snooze_config['duration'] + # Increment snooze count and set next trigger time + alarm_info['snooze_count'] = alarm_info.get('snooze_count', 0) + 1 + snooze_duration = snooze_config.get('duration', 5) alarm_info['trigger_time'] = datetime.now() + timedelta(minutes=snooze_duration) - logger.info(f"Snoozed alarm {alarm_id} for {snooze_duration} minutes. Snooze count: {alarm_info['snooze_count']}") + # Stop any active playback + process = alarm_info.get('process') + if process: + process.terminate() + process.wait() + alarm_info['process'] = None + + logger.info(f"Snoozed alarm {alarm_id} for {snooze_duration} minutes.") return True + +def dismiss_alarm(self, alarm_id: int): + """Dismiss a specific alarm.""" + if alarm_id not in self.active_alarms: + logger.warning(f"Cannot dismiss alarm {alarm_id} - not found in active alarms") + return False + + # Stop playback + alarm_info = self.active_alarms.pop(alarm_id, None) + process = alarm_info.get('process') + if process: + process.terminate() + process.wait() + + logger.info(f"Dismissed alarm {alarm_id}.") + return True + diff --git a/alert_api/data_classes.py b/alert_api/data_classes.py index 6273d73..cb4ebea 100644 --- a/alert_api/data_classes.py +++ b/alert_api/data_classes.py @@ -4,20 +4,10 @@ from typing import List, Optional, Dict, Any from datetime import datetime import os import re -import logging - -# Set up logging configuration -logging.basicConfig( - level=logging.DEBUG, # Set to DEBUG to show all log levels - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.StreamHandler(), # Console handler - logging.FileHandler('alert_api.log') # File handler - ] -) - -logger = logging.getLogger('AlertApi') +from logging_config import setup_logging +# Set up logging +logger = setup_logging() @dataclass class RepeatRule: diff --git a/alert_api/main.py b/alert_api/main.py index fb618b2..334def3 100755 --- a/alert_api/main.py +++ b/alert_api/main.py @@ -56,7 +56,7 @@ class AlarmSystemManager: self._sync_alarms() # UI.. - self.ui = UI(self) + self.ui = UI(self, control_queue=self.siren.control_queue) def _setup_signal_handlers(self): """Set up signal handlers for graceful shutdown""" diff --git a/alert_api/ncurses_ui.py b/alert_api/ncurses_ui.py index e2eaea2..cdf7d35 100644 --- a/alert_api/ncurses_ui.py +++ b/alert_api/ncurses_ui.py @@ -3,26 +3,33 @@ import time from datetime import datetime, date, timedelta import threading import logging +import queue # Import drawing methods from the new module from ncurses_ui_draw import ( _draw_main_clock, _draw_add_alarm, _draw_list_alarms, + _draw_active_alarms, _draw_error ) -# class AlarmClockUI: class UI: - def __init__(self, alarm_system_manager): + def __init__(self, alarm_system_manager, control_queue): # UI State Management self.alarm_system = alarm_system_manager self.stop_event = alarm_system_manager.stop_event self.storage = alarm_system_manager.storage + # Control queue for interacting with AlarmSiren + self.control_queue = control_queue + # Logging self.logger = logging.getLogger(__name__) + # Active alarm tracking + self.active_alarms = {} + # UI State self.reset_ui_state() @@ -54,10 +61,17 @@ class UI: # Weekday names (to match specification) self.weekday_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + # Clear active alarms + self.active_alarms.clear() + def run(self): """Start the ncurses UI in a separate thread""" def ui_thread(): try: + # Start a thread to monitor control queue + monitor_thread = threading.Thread(target=self._monitor_control_queue, daemon=True) + monitor_thread.start() + curses.wrapper(self._main_loop) except Exception as e: self.logger.error(f"UI Thread Error: {e}") @@ -68,6 +82,66 @@ class UI: ui_thread_obj.start() return ui_thread_obj + def _monitor_control_queue(self): + """Monitor the control queue for alarm events""" + while not self.stop_event.is_set(): + try: + # Non-blocking check of control queue + try: + control_msg = self.control_queue.get(timeout=1) + + # Handle different types of control messages + if control_msg['type'] == 'trigger': + # Store triggered alarm + alarm_id = control_msg['alarm_id'] + self.active_alarms[alarm_id] = control_msg['info'] + + # If not already in alarm view, switch to it + if self.current_view != 'ACTIVE_ALARMS': + self.current_view = 'ACTIVE_ALARMS' + + except queue.Empty: + pass + + time.sleep(0.1) + except Exception as e: + self.logger.error(f"Error monitoring control queue: {e}") + time.sleep(1) + + def _handle_active_alarms_input(self, key): + """Handle input for active alarms view""" + if not self.active_alarms: + # No active alarms, return to clock view + self.current_view = 'CLOCK' + return + + # Get the first (or only) active alarm + alarm_id = list(self.active_alarms.keys())[0] + + if key == ord('s'): # Snooze + # Send snooze command to control queue + self.control_queue.put({ + 'type': 'snooze', + 'alarm_id': alarm_id + }) + # Remove from active alarms + del self.active_alarms[alarm_id] + + # Optional: show a snooze confirmation + self._show_error("Alarm Snoozed") + + elif key == ord('d'): # Dismiss + # Send dismiss command to control queue + self.control_queue.put({ + 'type': 'dismiss', + 'alarm_id': alarm_id + }) + # Remove from active alarms + del self.active_alarms[alarm_id] + + # Return to clock view + self.current_view = 'CLOCK' + def _handle_clock_input(self, key): """Handle input on the clock view""" if key == ord('a'): @@ -199,6 +273,9 @@ class UI: 'alarms': self.alarm_list or [], 'weekday_names': self.weekday_names }) + elif self.current_view == 'ACTIVE_ALARMS': + # Draw active alarm view + _draw_active_alarms(stdscr, {'active_alarms': self.active_alarms }) # Render error if exists if self.error_message: @@ -225,6 +302,8 @@ class UI: self._handle_add_alarm_input(key) elif self.current_view == 'LIST_ALARMS': self._handle_list_alarms_input(key) + elif self.current_view == 'ACTIVE_ALARMS': + self._handle_active_alarms_input(key) time.sleep(0.2) diff --git a/alert_api/ncurses_ui_draw.py b/alert_api/ncurses_ui_draw.py index 713b9ec..e8f6cbb 100644 --- a/alert_api/ncurses_ui_draw.py +++ b/alert_api/ncurses_ui_draw.py @@ -27,6 +27,32 @@ def _draw_error(stdscr, error_message): stdscr.addstr(error_y, error_x, error_message) stdscr.attroff(curses.color_pair(3) | curses.A_BOLD) +def _draw_active_alarms(stdscr, context): + """Draw the active alarms screen""" + height, width = stdscr.getmaxyx() + + active_alarms = context.get('active_alarms', {}) + + if not active_alarms: + return + + # Get the first (or only) active alarm + alarm_id = list(active_alarms.keys())[0] + alarm_info = active_alarms[alarm_id] + alarm_config = alarm_info['config'] + + # Draw alarm details + stdscr.addstr(height // 2 - 2, width // 2 - 10, "ALARM TRIGGERED") + stdscr.addstr(height // 2, width // 2 - len(alarm_config.get('name', 'Unnamed Alarm'))//2, + alarm_config.get('name', 'Unnamed Alarm')) + + # Time info + time_str = alarm_config.get('time', 'Unknown Time') + stdscr.addstr(height // 2 + 2, width // 2 - len(time_str)//2, time_str) + + # Instructions + stdscr.addstr(height - 2, width // 2 - 15, "S: Snooze D: Dismiss") + def _draw_big_digit(stdscr, y, x, digit): """Draw a big digit using predefined patterns""" patterns = BIG_DIGITS[digit]