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):
|
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)
|
||||||
|
@ -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
|
||||||
@ -183,33 +195,48 @@ class AlarmSiren:
|
|||||||
|
|
||||||
# Default snooze configuration if not provided
|
# Default snooze configuration if not provided
|
||||||
snooze_config = alarm_config.get('snooze', {
|
snooze_config = alarm_config.get('snooze', {
|
||||||
'enabled': True,
|
'enabled': True,
|
||||||
'duration': 5, # Default 5 minutes
|
'duration': 5, # Default 5 minutes
|
||||||
'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
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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"""
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user