195 lines
7.2 KiB
Python
195 lines
7.2 KiB
Python
"""
|
||
Alarm clock module.
|
||
Handles alarm scheduling, persistence, and playback.
|
||
"""
|
||
import json
|
||
import time
|
||
import subprocess
|
||
import re
|
||
import threading
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from config import BASE_DIR
|
||
from local_stt import listen_for_keywords
|
||
|
||
ALARM_FILE = BASE_DIR / "alarms.json"
|
||
ALARM_SOUND = BASE_DIR / "Apex-1.mp3"
|
||
|
||
class AlarmClock:
|
||
def __init__(self):
|
||
self.alarms = []
|
||
self.load_alarms()
|
||
|
||
def load_alarms(self):
|
||
"""Load alarms from JSON file."""
|
||
if ALARM_FILE.exists():
|
||
try:
|
||
with open(ALARM_FILE, "r", encoding="utf-8") as f:
|
||
self.alarms = json.load(f)
|
||
except Exception as e:
|
||
print(f"❌ Ошибка загрузки будильников: {e}")
|
||
self.alarms = []
|
||
|
||
def save_alarms(self):
|
||
"""Save alarms to JSON file."""
|
||
try:
|
||
with open(ALARM_FILE, "w", encoding="utf-8") as f:
|
||
json.dump(self.alarms, f, indent=4)
|
||
except Exception as e:
|
||
print(f"❌ Ошибка сохранения будильников: {e}")
|
||
|
||
def add_alarm(self, hour: int, minute: int):
|
||
"""Add a new alarm."""
|
||
# Check if already exists
|
||
for alarm in self.alarms:
|
||
if alarm["hour"] == hour and alarm["minute"] == minute:
|
||
alarm["active"] = True
|
||
self.save_alarms()
|
||
return
|
||
|
||
self.alarms.append({
|
||
"hour": hour,
|
||
"minute": minute,
|
||
"active": True
|
||
})
|
||
self.save_alarms()
|
||
print(f"⏰ Будильник установлен на {hour:02d}:{minute:02d}")
|
||
|
||
def cancel_all_alarms(self):
|
||
"""Cancel all active alarms."""
|
||
for alarm in self.alarms:
|
||
alarm["active"] = False
|
||
self.save_alarms()
|
||
print("🔕 Все будильники отменены.")
|
||
|
||
def check_alarms(self):
|
||
"""Check if any alarm should trigger now. Returns True if triggered."""
|
||
now = datetime.now()
|
||
triggered = False
|
||
|
||
for alarm in self.alarms:
|
||
if alarm["active"]:
|
||
if alarm["hour"] == now.hour and alarm["minute"] == now.minute:
|
||
# Prevent re-triggering within the same minute?
|
||
# We should disable it immediately or track last trigger time.
|
||
# For simple logic: disable it (one-time alarm).
|
||
|
||
# But wait, checking every second?
|
||
# If I disable it, it won't ring for the whole minute.
|
||
# Correct.
|
||
print(f"⏰ ВРЕМЯ БУДИЛЬНИКА: {alarm['hour']:02d}:{alarm['minute']:02d}")
|
||
alarm["active"] = False
|
||
triggered = True
|
||
self.trigger_alarm()
|
||
break # Trigger one at a time
|
||
|
||
if triggered:
|
||
self.save_alarms()
|
||
return True
|
||
return False
|
||
|
||
def trigger_alarm(self):
|
||
"""Play alarm sound and wait for stop command."""
|
||
print("🔔 БУДИЛЬНИК ЗВОНИТ! (Скажите 'Стоп' или 'Александр стоп')")
|
||
|
||
# Start playing sound in loop
|
||
# -q for quiet (no output)
|
||
# --loop -1 for infinite loop
|
||
cmd = ["mpg123", "-q", "--loop", "-1", str(ALARM_SOUND)]
|
||
|
||
try:
|
||
process = subprocess.Popen(cmd)
|
||
except FileNotFoundError:
|
||
print("❌ Ошибка: mpg123 не найден. Установите его: sudo apt install mpg123")
|
||
return
|
||
|
||
try:
|
||
# Listen for stop command using local Vosk
|
||
# Loop until stop word is heard
|
||
stop_words = ["стоп", "хватит", "тихо", "замолчи", "отмена", "александр стоп"]
|
||
|
||
while True:
|
||
# Listen in short bursts to be responsive
|
||
text = listen_for_keywords(stop_words, timeout=3.0)
|
||
if text:
|
||
print(f"🛑 Будильник остановлен по команде: '{text}'")
|
||
break
|
||
|
||
except Exception as e:
|
||
print(f"❌ Ошибка во время будильника: {e}")
|
||
finally:
|
||
# Kill the player
|
||
process.terminate()
|
||
try:
|
||
process.wait(timeout=1)
|
||
except subprocess.TimeoutExpired:
|
||
process.kill()
|
||
print("🔕 Будильник выключен.")
|
||
|
||
def parse_command(self, text: str) -> str | None:
|
||
"""
|
||
Parse user text for alarm commands.
|
||
Returns response string if command handled, None otherwise.
|
||
"""
|
||
text = text.lower()
|
||
if "будильник" not in text and "разбуди" not in text:
|
||
return None
|
||
|
||
if "отмени" in text:
|
||
self.cancel_all_alarms()
|
||
return "Хорошо, я отменил все будильники."
|
||
|
||
# Regex to find time: HH:MM, HH-MM, HH MM, HH часов MM минут
|
||
# 1. "07:30", "7:30"
|
||
match = re.search(r'\b(\d{1,2})[:.-](\d{2})\b', text)
|
||
if match:
|
||
h, m = int(match.group(1)), int(match.group(2))
|
||
if 0 <= h <= 23 and 0 <= m <= 59:
|
||
self.add_alarm(h, m)
|
||
return f"Я установил будильник на {h} часов {m} минут."
|
||
|
||
# 2. "7 часов 30 минут" or "7 30"
|
||
# Search for pattern: digits ... (digits)?
|
||
# Complex to separate from other numbers.
|
||
|
||
# Simple heuristics:
|
||
words = text.split()
|
||
nums = [int(s) for s in text.split() if s.isdigit()]
|
||
|
||
# "на 7" -> 7:00
|
||
if "на" in words or "в" in words:
|
||
# Try to find number after preposition
|
||
pass
|
||
|
||
# Let's rely on explicit digit search if regex failed
|
||
# Patterns: "на 8", "на 8 30", "на 8 часов 30 минут", "на 8 часов"
|
||
|
||
# Regex to capture hour and optional minute
|
||
# Matches: "на <H> [часов] [M] [минут]"
|
||
match_time = re.search(r'на\s+(\d{1,2})(?:\s*(?:часов|часа|час))?(?:\s+(\d{1,2})(?:\s*(?:минут|минуты|минута))?)?', text)
|
||
|
||
if match_time:
|
||
h = int(match_time.group(1))
|
||
m = int(match_time.group(2)) if match_time.group(2) else 0
|
||
|
||
# Handle AM/PM if specified
|
||
if "вечера" in text and h < 12:
|
||
h += 12
|
||
elif "утра" in text and h == 12:
|
||
h = 0
|
||
|
||
if 0 <= h <= 23 and 0 <= m <= 59:
|
||
self.add_alarm(h, m)
|
||
return f"Хорошо, разбужу вас в {h}:{m:02d}."
|
||
|
||
return "Я не понял время для будильника. Пожалуйста, скажите точное время, например 'семь тридцать'."
|
||
|
||
# Global instance
|
||
_alarm_clock = None
|
||
|
||
def get_alarm_clock():
|
||
global _alarm_clock
|
||
if _alarm_clock is None:
|
||
_alarm_clock = AlarmClock()
|
||
return _alarm_clock
|