Files
smart-speaker/alarm.py

195 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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