Active alarms almost work.
This commit is contained in:
parent
f3dabc1116
commit
6fd172ce2e
@ -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)
|
||||
|
@ -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
|
||||
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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"""
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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]
|
||||
|
Loading…
x
Reference in New Issue
Block a user