import asyncio import logging import random from datetime import datetime from zoneinfo import ZoneInfo import httpx from app.config import get_settings from app.db.base import SessionLocal from app.homelab.comfyui import ComfyUIClient from app.homelab.context import resolve_timezone from app.homelab.digest import build_morning_digest from app.homelab.monitoring import check_netdata_alerts from app.homelab.notices import post_chat_notice from app.homelab.state import get_state, set_state logger = logging.getLogger(__name__) WATCH_INTERVAL_SEC = 60 _netdata_tick = 0 async def homelab_watcher_loop() -> None: global _netdata_tick while True: try: await asyncio.sleep(WATCH_INTERVAL_SEC) await _tick_morning_digest() await _tick_rofl() settings = get_settings() _netdata_tick += WATCH_INTERVAL_SEC if _netdata_tick >= settings.netdata_poll_interval_sec: _netdata_tick = 0 await _tick_netdata() except asyncio.CancelledError: raise except Exception: logger.exception("Homelab watcher error") async def _tick_morning_digest() -> None: settings = get_settings() if not settings.morning_digest_enabled: return db = SessionLocal() try: tz_name = resolve_timezone(db) try: tz = ZoneInfo(tz_name) except Exception: tz = ZoneInfo("Europe/Moscow") now = datetime.now(tz) target_min = settings.morning_digest_hour * 60 + settings.morning_digest_minute current_min = now.hour * 60 + now.minute if current_min < target_min or current_min >= target_min + 3: return today = now.date().isoformat() if get_state(db, "last_morning_digest_date") == today: return digest = build_morning_digest(db, include_news=True) post_chat_notice(digest) set_state(db, "last_morning_digest_date", today) finally: db.close() async def _tick_netdata() -> None: db = SessionLocal() try: for notice in check_netdata_alerts(db): post_chat_notice(notice) finally: db.close() async def _comfyui_reachable(base_url: str) -> bool: try: async with httpx.AsyncClient(timeout=httpx.Timeout(3.0, connect=2.0)) as client: response = await client.get(f"{base_url.rstrip('/')}/system_stats") return response.status_code < 500 except (httpx.TimeoutException, httpx.ConnectError, httpx.NetworkError): return False async def _tick_rofl() -> None: settings = get_settings() if not settings.comfyui_enabled or not settings.comfyui_rofl_enabled: return db = SessionLocal() try: tz_name = resolve_timezone(db) try: tz = ZoneInfo(tz_name) except Exception: tz = ZoneInfo("Europe/Moscow") now = datetime.now(tz) last_raw = get_state(db, "last_comfy_rofl_at") if last_raw: try: last_at = datetime.fromisoformat(last_raw) if last_at.tzinfo is None: last_at = last_at.replace(tzinfo=tz) if (now - last_at).total_seconds() < settings.comfyui_rofl_min_interval_hours * 3600: return except ValueError: pass if random.random() > settings.comfyui_rofl_probability: return today = now.date().isoformat() count_raw = get_state(db, f"comfy_rofl_count_{today}") or "0" try: count = int(count_raw) except ValueError: count = 0 if count >= settings.comfyui_rofl_max_per_day: return client = ComfyUIClient() if not await _comfyui_reachable(client.base_url): return prompt = client.random_rofl_prompt() try: result = await asyncio.wait_for( client.generate_image(prompt), timeout=settings.comfyui_timeout_sec + 15, ) except (asyncio.TimeoutError, httpx.TimeoutException, httpx.ConnectError) as exc: logger.warning("Rofl image skipped (ComfyUI): %s", exc) return if not result.get("ok"): logger.warning("Rofl image failed: %s", result.get("error")) return url = result.get("url", "") post_chat_notice( f"🎨 **Рофл дня**\n\n![rofl]({url})\n\n_{prompt}_" ) set_state(db, f"comfy_rofl_count_{today}", str(count + 1)) set_state(db, "last_comfy_rofl_at", now.isoformat()) finally: db.close()