Active alarms almost work.

This commit is contained in:
Kalzu Rekku 2025-01-26 22:02:23 +02:00
parent f3dabc1116
commit 6fd172ce2e
6 changed files with 174 additions and 63 deletions

View File

@ -134,17 +134,6 @@ class AlertApi(BaseHTTPRequestHandler):
def run(server_class=HTTPServer, handler_class=AlertApi, port=8000): 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}") logger.info(f"Starting AlertApi on port {port}")
server_address = ("", port) server_address = ("", port)

View File

@ -100,12 +100,11 @@ class AlarmSiren:
return None return None
def _play_audio(self, file_path: str, volume: int = 100): def _play_audio(self, file_path: str, volume: int = 100):
"""Play audio file using mpg123""" """Play audio file using mpg123 in the background."""
try: try:
# Ensure the file exists
if not os.path.exists(file_path): if not os.path.exists(file_path):
logger.error(f"Audio file not found: {file_path}") logger.error(f"Audio file not found: {file_path}")
return False return None
# Construct mpg123 command with volume control # Construct mpg123 command with volume control
volume_adjust = f"-g {volume}" volume_adjust = f"-g {volume}"
@ -113,12 +112,12 @@ class AlarmSiren:
logger.info(f"Playing alarm: {file_path}") logger.info(f"Playing alarm: {file_path}")
# Track the process for potential interruption # Run mpg123 in the background, suppressing stdout/stderr
process = subprocess.Popen(cmd) process = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return process return process
except Exception as e: except Exception as e:
logger.error(f"Error playing audio: {e}") logger.error(f"Error playing audio: {e}")
return False return None
def _playback_worker(self): def _playback_worker(self):
"""Background thread for managing alarm playback""" """Background thread for managing alarm playback"""
@ -129,10 +128,12 @@ class AlarmSiren:
new_alarm = self.alarm_queue.get(timeout=1) new_alarm = self.alarm_queue.get(timeout=1)
alarm_time = self._calculate_next_alarm_time(new_alarm) alarm_time = self._calculate_next_alarm_time(new_alarm)
if alarm_time: 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, 'config': new_alarm,
'trigger_time': alarm_time, 'trigger_time': alarm_time,
'snooze_count': 0 'snooze_count': 0,
'process': None
} }
except queue.Empty: except queue.Empty:
pass pass
@ -140,7 +141,11 @@ class AlarmSiren:
# Check for control signals (snooze/dismiss) # Check for control signals (snooze/dismiss)
try: try:
control_msg = self.control_queue.get(timeout=0.1) 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: except queue.Empty:
pass pass
@ -148,23 +153,30 @@ class AlarmSiren:
now = datetime.now() now = datetime.now()
for alarm_id, alarm_info in list(self.active_alarms.items()): for alarm_id, alarm_info in list(self.active_alarms.items()):
if now >= alarm_info['trigger_time']: if now >= alarm_info['trigger_time']:
# Trigger alarm # Trigger alarm if not already active
process = self._play_audio( if alarm_info['process'] is None:
alarm_info['config']['file_to_play'], alarm_info['process'] = self._play_audio(
alarm_info['config'].get('metadata', {}).get('volume', 100) 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 # Notify UI about the triggered alarm
if process: self.control_queue.put({
# Wait for user interaction or timeout 'type': 'trigger',
# In a real implementation, this would be more sophisticated 'alarm_id': alarm_id,
time.sleep(30) # Placeholder for user interaction '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']) next_trigger = self._calculate_next_alarm_time(alarm_info['config'])
if next_trigger: if next_trigger:
alarm_info['trigger_time'] = next_trigger alarm_info['trigger_time'] = next_trigger
alarm_info['process'] = None
else: else:
logger.info(f"Removing non-repeating alarm {alarm_id}.")
del self.active_alarms[alarm_id] del self.active_alarms[alarm_id]
time.sleep(1) # Prevent tight loop time.sleep(1) # Prevent tight loop
@ -188,28 +200,43 @@ class AlarmSiren:
'max_count': 3 # Default max 3 snoozes 'max_count': 3 # Default max 3 snoozes
}) })
# Ensure all required keys exist with default values if not snooze_config.get('enabled', True):
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']:
logger.warning(f"Snooze not enabled for alarm {alarm_id}") logger.warning(f"Snooze not enabled for alarm {alarm_id}")
return False return False
# Check snooze count # Check snooze count
current_snooze_count = alarm_info.get('snooze_count', 0) if alarm_info.get('snooze_count', 0) >= snooze_config['max_count']:
if current_snooze_count >= snooze_config['max_count']:
logger.warning(f"Maximum snooze count reached for alarm {alarm_id}") logger.warning(f"Maximum snooze count reached for alarm {alarm_id}")
return False return False
# Increment snooze count # Increment snooze count and set next trigger time
alarm_info['snooze_count'] = current_snooze_count + 1 alarm_info['snooze_count'] = alarm_info.get('snooze_count', 0) + 1
snooze_duration = snooze_config.get('duration', 5)
# Set next trigger time
snooze_duration = snooze_config['duration']
alarm_info['trigger_time'] = datetime.now() + timedelta(minutes=snooze_duration) 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 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

View File

@ -4,20 +4,10 @@ from typing import List, Optional, Dict, Any
from datetime import datetime from datetime import datetime
import os import os
import re import re
import logging from logging_config import setup_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')
# Set up logging
logger = setup_logging()
@dataclass @dataclass
class RepeatRule: class RepeatRule:

View File

@ -56,7 +56,7 @@ class AlarmSystemManager:
self._sync_alarms() self._sync_alarms()
# UI.. # UI..
self.ui = UI(self) self.ui = UI(self, control_queue=self.siren.control_queue)
def _setup_signal_handlers(self): def _setup_signal_handlers(self):
"""Set up signal handlers for graceful shutdown""" """Set up signal handlers for graceful shutdown"""

View File

@ -3,26 +3,33 @@ import time
from datetime import datetime, date, timedelta from datetime import datetime, date, timedelta
import threading import threading
import logging import logging
import queue
# Import drawing methods from the new module # Import drawing methods from the new module
from ncurses_ui_draw import ( from ncurses_ui_draw import (
_draw_main_clock, _draw_main_clock,
_draw_add_alarm, _draw_add_alarm,
_draw_list_alarms, _draw_list_alarms,
_draw_active_alarms,
_draw_error _draw_error
) )
# class AlarmClockUI:
class UI: class UI:
def __init__(self, alarm_system_manager): def __init__(self, alarm_system_manager, control_queue):
# UI State Management # UI State Management
self.alarm_system = alarm_system_manager self.alarm_system = alarm_system_manager
self.stop_event = alarm_system_manager.stop_event self.stop_event = alarm_system_manager.stop_event
self.storage = alarm_system_manager.storage self.storage = alarm_system_manager.storage
# Control queue for interacting with AlarmSiren
self.control_queue = control_queue
# Logging # Logging
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
# Active alarm tracking
self.active_alarms = {}
# UI State # UI State
self.reset_ui_state() self.reset_ui_state()
@ -54,10 +61,17 @@ class UI:
# Weekday names (to match specification) # Weekday names (to match specification)
self.weekday_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] self.weekday_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
# Clear active alarms
self.active_alarms.clear()
def run(self): def run(self):
"""Start the ncurses UI in a separate thread""" """Start the ncurses UI in a separate thread"""
def ui_thread(): def ui_thread():
try: 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) curses.wrapper(self._main_loop)
except Exception as e: except Exception as e:
self.logger.error(f"UI Thread Error: {e}") self.logger.error(f"UI Thread Error: {e}")
@ -68,6 +82,66 @@ class UI:
ui_thread_obj.start() ui_thread_obj.start()
return ui_thread_obj 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): def _handle_clock_input(self, key):
"""Handle input on the clock view""" """Handle input on the clock view"""
if key == ord('a'): if key == ord('a'):
@ -199,6 +273,9 @@ class UI:
'alarms': self.alarm_list or [], 'alarms': self.alarm_list or [],
'weekday_names': self.weekday_names '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 # Render error if exists
if self.error_message: if self.error_message:
@ -225,6 +302,8 @@ class UI:
self._handle_add_alarm_input(key) self._handle_add_alarm_input(key)
elif self.current_view == 'LIST_ALARMS': elif self.current_view == 'LIST_ALARMS':
self._handle_list_alarms_input(key) self._handle_list_alarms_input(key)
elif self.current_view == 'ACTIVE_ALARMS':
self._handle_active_alarms_input(key)
time.sleep(0.2) time.sleep(0.2)

View File

@ -27,6 +27,32 @@ def _draw_error(stdscr, error_message):
stdscr.addstr(error_y, error_x, error_message) stdscr.addstr(error_y, error_x, error_message)
stdscr.attroff(curses.color_pair(3) | curses.A_BOLD) 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): def _draw_big_digit(stdscr, y, x, digit):
"""Draw a big digit using predefined patterns""" """Draw a big digit using predefined patterns"""
patterns = BIG_DIGITS[digit] patterns = BIG_DIGITS[digit]