Update assistant features and docs
This commit is contained in:
267
app/features/stopwatch.py
Normal file
267
app/features/stopwatch.py
Normal file
@@ -0,0 +1,267 @@
|
||||
"""Stopwatch module."""
|
||||
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from ..core.config import BASE_DIR
|
||||
|
||||
STOPWATCH_FILE = BASE_DIR / "data" / "stopwatches.json"
|
||||
|
||||
|
||||
# Optional ordinal formatting for list numbering.
|
||||
try:
|
||||
from num2words import num2words
|
||||
except Exception:
|
||||
num2words = None
|
||||
|
||||
|
||||
def _format_ordinal_index(index: int) -> str:
|
||||
if num2words is None:
|
||||
return f"{index}-й"
|
||||
try:
|
||||
return num2words(index, lang="ru", to="ordinal", case="nominative", gender="m")
|
||||
except Exception:
|
||||
return f"{index}-й"
|
||||
|
||||
|
||||
def _format_duration(seconds: float) -> str:
|
||||
total = int(round(max(0, seconds)))
|
||||
hours = total // 3600
|
||||
minutes = (total % 3600) // 60
|
||||
sec = total % 60
|
||||
|
||||
parts = []
|
||||
if hours:
|
||||
parts.append(f"{hours} ч")
|
||||
if minutes:
|
||||
parts.append(f"{minutes} мин")
|
||||
parts.append(f"{sec} сек")
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
class StopwatchManager:
|
||||
def __init__(self):
|
||||
self.stopwatches = []
|
||||
self.load_stopwatches()
|
||||
|
||||
def load_stopwatches(self):
|
||||
if not STOPWATCH_FILE.exists():
|
||||
return
|
||||
try:
|
||||
with open(STOPWATCH_FILE, "r", encoding="utf-8") as f:
|
||||
raw = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка загрузки секундомеров: {e}")
|
||||
return
|
||||
|
||||
items = []
|
||||
for item in raw:
|
||||
try:
|
||||
stopwatch_id = int(item["id"])
|
||||
except Exception:
|
||||
continue
|
||||
items.append(
|
||||
{
|
||||
"id": stopwatch_id,
|
||||
"name": str(item.get("name", "")).strip(),
|
||||
"elapsed": float(item.get("elapsed", 0)),
|
||||
"running": bool(item.get("running", False)),
|
||||
"started_at": item.get("started_at"),
|
||||
}
|
||||
)
|
||||
self.stopwatches = sorted(items, key=lambda x: x["id"])
|
||||
|
||||
def save_stopwatches(self):
|
||||
payload = [
|
||||
{
|
||||
"id": sw["id"],
|
||||
"name": sw.get("name", ""),
|
||||
"elapsed": sw.get("elapsed", 0),
|
||||
"running": sw.get("running", False),
|
||||
"started_at": sw.get("started_at"),
|
||||
}
|
||||
for sw in self.stopwatches
|
||||
]
|
||||
try:
|
||||
with open(STOPWATCH_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(payload, f, indent=4)
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка сохранения секундомеров: {e}")
|
||||
|
||||
def _next_id(self) -> int:
|
||||
if not self.stopwatches:
|
||||
return 1
|
||||
return max(sw["id"] for sw in self.stopwatches) + 1
|
||||
|
||||
def _now_iso(self) -> str:
|
||||
return datetime.now().isoformat()
|
||||
|
||||
def _elapsed_now(self, stopwatch: dict) -> float:
|
||||
elapsed = float(stopwatch.get("elapsed", 0))
|
||||
if not stopwatch.get("running"):
|
||||
return elapsed
|
||||
|
||||
started_at = stopwatch.get("started_at")
|
||||
if not started_at:
|
||||
return elapsed
|
||||
|
||||
try:
|
||||
started_dt = datetime.fromisoformat(started_at)
|
||||
except Exception:
|
||||
return elapsed
|
||||
|
||||
delta = (datetime.now() - started_dt).total_seconds()
|
||||
return elapsed + max(0, delta)
|
||||
|
||||
def _running(self):
|
||||
return [sw for sw in self.stopwatches if sw.get("running")]
|
||||
|
||||
def _paused(self):
|
||||
return [sw for sw in self.stopwatches if not sw.get("running")]
|
||||
|
||||
def has_running_stopwatches(self) -> bool:
|
||||
return bool(self._running())
|
||||
|
||||
def describe_active_stopwatches(self) -> str:
|
||||
running = self._running()
|
||||
if not running:
|
||||
return "Активных секундомеров нет."
|
||||
|
||||
running.sort(key=lambda sw: sw["id"])
|
||||
items = []
|
||||
for idx, sw in enumerate(running, start=1):
|
||||
ordinal = _format_ordinal_index(idx)
|
||||
duration = _format_duration(self._elapsed_now(sw))
|
||||
name = sw.get("name", "")
|
||||
if name:
|
||||
items.append(f"{ordinal}) {name} — {duration}")
|
||||
else:
|
||||
items.append(f"{ordinal}) {duration}")
|
||||
return "Активные секундомеры: " + "; ".join(items) + "."
|
||||
|
||||
def start_stopwatch(self, name: str = "") -> str:
|
||||
stopwatch = {
|
||||
"id": self._next_id(),
|
||||
"name": name.strip(),
|
||||
"elapsed": 0.0,
|
||||
"running": True,
|
||||
"started_at": self._now_iso(),
|
||||
}
|
||||
self.stopwatches.append(stopwatch)
|
||||
self.save_stopwatches()
|
||||
if stopwatch["name"]:
|
||||
return f"Запустил секундомер «{stopwatch['name']}»."
|
||||
return "Запустил секундомер."
|
||||
|
||||
def pause_stopwatches(self) -> str:
|
||||
running = self._running()
|
||||
if not running:
|
||||
return "Сейчас нет активных секундомеров."
|
||||
|
||||
elapsed_items = []
|
||||
for sw in running:
|
||||
elapsed_now = self._elapsed_now(sw)
|
||||
elapsed_items.append(
|
||||
{
|
||||
"id": sw["id"],
|
||||
"name": sw.get("name", ""),
|
||||
"elapsed": elapsed_now,
|
||||
}
|
||||
)
|
||||
sw["elapsed"] = elapsed_now
|
||||
sw["running"] = False
|
||||
sw["started_at"] = None
|
||||
|
||||
self.save_stopwatches()
|
||||
count = len(running)
|
||||
if count == 1:
|
||||
elapsed_text = _format_duration(elapsed_items[0]["elapsed"])
|
||||
return f"Остановил секундомер. Он работал {elapsed_text}."
|
||||
|
||||
details = []
|
||||
for idx, item in enumerate(sorted(elapsed_items, key=lambda x: x["id"]), start=1):
|
||||
ordinal = _format_ordinal_index(idx)
|
||||
elapsed_text = _format_duration(item["elapsed"])
|
||||
name = item.get("name", "")
|
||||
if name:
|
||||
details.append(f"{ordinal} «{name}» — {elapsed_text}")
|
||||
else:
|
||||
details.append(f"{ordinal} — {elapsed_text}")
|
||||
return f"Остановил секундомеры: {count} шт. Время: " + "; ".join(details) + "."
|
||||
|
||||
def resume_stopwatches(self) -> str:
|
||||
paused = self._paused()
|
||||
if not paused:
|
||||
return "Пауза не активна: секундомеры уже запущены или отсутствуют."
|
||||
|
||||
for sw in paused:
|
||||
sw["running"] = True
|
||||
sw["started_at"] = self._now_iso()
|
||||
|
||||
self.save_stopwatches()
|
||||
count = len(paused)
|
||||
if count == 1:
|
||||
return "Продолжил секундомер."
|
||||
return f"Продолжил секундомеры: {count} шт."
|
||||
|
||||
def reset_stopwatches(self) -> str:
|
||||
if not self.stopwatches:
|
||||
return "Секундомеров для сброса нет."
|
||||
|
||||
count = len(self.stopwatches)
|
||||
self.stopwatches = []
|
||||
self.save_stopwatches()
|
||||
if count == 1:
|
||||
return "Секундомер сброшен."
|
||||
return f"Сбросил секундомеры: {count} шт."
|
||||
|
||||
def parse_command(self, text: str) -> str | None:
|
||||
text = text.lower().strip()
|
||||
|
||||
has_stopwatch_word = any(
|
||||
word in text
|
||||
for word in [
|
||||
"секундомер",
|
||||
"секундомеры",
|
||||
"секундомером",
|
||||
"секундомера",
|
||||
"секундомеру",
|
||||
]
|
||||
)
|
||||
if not has_stopwatch_word:
|
||||
return None
|
||||
|
||||
if re.search(r"(какие|какой|список|активн|покажи|сколько|есть ли)", text):
|
||||
return self.describe_active_stopwatches()
|
||||
|
||||
if any(word in text for word in ["сброс", "удали", "отмени", "очист"]):
|
||||
return self.reset_stopwatches()
|
||||
|
||||
if any(word in text for word in ["продолж", "возобнов"]):
|
||||
return self.resume_stopwatches()
|
||||
|
||||
if any(word in text for word in ["стоп", "останов", "пауза"]):
|
||||
return self.pause_stopwatches()
|
||||
|
||||
if "постав" in text or "установ" in text:
|
||||
return self.start_stopwatch()
|
||||
|
||||
if any(word in text for word in ["запусти", "включи", "старт", "начни"]):
|
||||
return self.start_stopwatch()
|
||||
|
||||
# Если пользователь просто сказал "секундомер", трактуем как запуск.
|
||||
if text in {"секундомер", "запусти секундомер", "включи секундомер"}:
|
||||
return self.start_stopwatch()
|
||||
|
||||
return "Я понял команду про секундомер, но не распознал действие. Скажите: запусти, стоп, продолжи, сбрось или покажи активные секундомеры."
|
||||
|
||||
|
||||
_stopwatch_manager = None
|
||||
|
||||
|
||||
def get_stopwatch_manager():
|
||||
global _stopwatch_manager
|
||||
if _stopwatch_manager is None:
|
||||
_stopwatch_manager = StopwatchManager()
|
||||
return _stopwatch_manager
|
||||
Reference in New Issue
Block a user