import asyncio import random import logging from datetime import datetime, time, timedelta from typing import Optional from config import Config from grok_client import GrokClient from template_manager import TemplateManager from notification_client import NotificationClient logger = logging.getLogger(__name__) class NotificationScheduler: """Handles scheduling of notifications with random intervals and silent time.""" def __init__(self, config: Config): self.config = config timezone_name = getattr(config, 'timezone', 'UTC') self.template_manager = TemplateManager(config.templates_dir, timezone_name) self.notification_client = NotificationClient(config) self.running = False self._timezone = None self._timezone_name = None self._init_timezone() def _init_timezone(self): """Initialize timezone from config.""" import pytz timezone_name = getattr(self.config, 'timezone', 'UTC') try: self._timezone = pytz.timezone(timezone_name) self._timezone_name = timezone_name except pytz.exceptions.UnknownTimeZoneError: logger.warning(f"Unknown timezone '{timezone_name}', falling back to UTC") self._timezone = pytz.UTC self._timezone_name = "UTC (fallback)" def _get_current_time(self): """Get current time in configured timezone.""" return datetime.now(self._timezone) def _is_silent_time(self) -> bool: """Check if current time is within silent hours.""" current_datetime = self._get_current_time() current_time = current_datetime.time() # Parse silent time configuration silent_start = datetime.strptime(self.config.silent_start, "%H:%M").time() silent_end = datetime.strptime(self.config.silent_end, "%H:%M").time() # Log current time for debugging logger.info(f"Current time: {current_datetime.strftime('%Y-%m-%d %H:%M:%S')} (timezone {self._timezone_name})") logger.info(f"Silent period: {self.config.silent_start} to {self.config.silent_end}") # Handle overnight silent period (e.g., 20:00 to 12:00) if silent_start >= silent_end: is_silent = current_time >= silent_start or current_time <= silent_end else: is_silent = silent_start <= current_time <= silent_end logger.info(f"Silent time check: {is_silent}") return is_silent def _get_next_interval(self) -> int: """Get random interval in seconds between min and max.""" minutes = random.randint(self.config.min_interval, self.config.max_interval) return minutes * 60 async def _send_notification(self) -> bool: """Send a notification with Grok response.""" try: async with GrokClient(self.config) as client: # Check if Grok is available if not await client.check_health(): logger.error("Grok service is not available") return False # Get random prompt template_data = self.template_manager.get_random_prompt() prompt = template_data["prompt"] title = template_data["title"] logger.info(f"Using prompt: {prompt}") logger.info(f"Notification title: {title}") # Get response from Grok strip_think_tags = getattr(self.config, 'strip_think_tags', True) response = await client.generate_response(prompt, strip_think_tags=strip_think_tags) if not response: logger.error("Failed to get response from Grok") return False # Send notification success = await self.notification_client.send_notification(response, title=title) if success: logger.info("Notification sent successfully") else: logger.error("Failed to send notification") return success except Exception as e: logger.error(f"Error sending notification: {e}") return False async def start(self): """Start the notification scheduler.""" self.running = True logger.info("Starting notification scheduler...") while self.running: try: # Check if it's silent time if self._is_silent_time(): logger.info("Silent time - skipping notification") # Wait 30 minutes before checking again await asyncio.sleep(1800) continue # Send notification await self._send_notification() # Calculate next interval interval = self._get_next_interval() current_time = self._get_current_time() next_time = current_time + timedelta(seconds=interval) logger.info(f"Next notification scheduled for: {next_time.strftime('%Y-%m-%d %H:%M:%S')} (timezone {self._timezone_name})") # Wait for next interval await asyncio.sleep(interval) except asyncio.CancelledError: logger.info("Scheduler cancelled") break except Exception as e: logger.error(f"Error in scheduler: {e}") # Wait 5 minutes on error await asyncio.sleep(300) def stop(self): """Stop the notification scheduler.""" self.running = False