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):
# 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)

View File

@ -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(
# 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
@ -188,28 +200,43 @@ class AlarmSiren:
'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

View File

@ -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:

View File

@ -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"""

View File

@ -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)

View File

@ -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]