import asyncio import logging import random from datetime import datetime from zoneinfo import ZoneInfo import httpx from sqlalchemy import select from app.config import get_settings from app.db.base import SessionLocal from app.db.models import User 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_scoped.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: users = db.scalars(select(User).where(User.is_active.is_(True))).all() digest = build_morning_digest(db, include_news=True) for user in users: tz_name = resolve_timezone(db, user.id) 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: continue today = now.date().isoformat() state_key = f"last_morning_digest_date:{user.id}" if get_state(db, state_key) == today: continue post_chat_notice(digest, user.id) set_state(db, state_key, today) finally: db.close() async def _tick_netdata() -> None: db = SessionLocal() try: notices = check_netdata_alerts(db) if not notices: return users = db.scalars(select(User).where(User.is_active.is_(True))).all() for user in users: for notice in notices: post_chat_notice(notice, user.id) 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: users = db.scalars(select(User).where(User.is_active.is_(True))).all() for user in users: tz_name = resolve_timezone(db, user.id) try: tz = ZoneInfo(tz_name) except Exception: tz = ZoneInfo("Europe/Moscow") now = datetime.now(tz) last_raw = get_state(db, f"last_comfy_rofl_at:{user.id}") 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: continue except ValueError: pass if random.random() > settings.comfyui_rofl_probability: continue today = now.date().isoformat() count_key = f"comfy_rofl_count_{today}:{user.id}" count_raw = get_state(db, count_key) or "0" try: count = int(count_raw) except ValueError: count = 0 if count >= settings.comfyui_rofl_max_per_day: continue client = ComfyUIClient() if not await _comfyui_reachable(client.base_url): continue 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) continue if not result.get("ok"): logger.warning("Rofl image failed: %s", result.get("error")) continue url = result.get("url", "") post_chat_notice( f"🎨 **Рофл дня**\n\n![rofl]({url})\n\n_{prompt}_", user.id, ) set_state(db, count_key, str(count + 1)) set_state(db, f"last_comfy_rofl_at:{user.id}", now.isoformat()) finally: db.close()