"""Discord a další notifikace pro provoz EMS.""" from __future__ import annotations import logging from datetime import datetime import asyncpg import httpx from app.config import get_settings logger = logging.getLogger(__name__) def _discord_level_for_mode_change(activated_by: str) -> str: if activated_by == "system:mismatch": return "critical" if activated_by.startswith("system:"): return "warning" return "info" async def notify_operating_mode_changed( site_code: str, previous_mode: str, new_mode: str, activated_by: str, notes: str | None, *, level: str | None = None, ) -> None: lvl = level or _discord_level_for_mode_change(activated_by) note_line = f"\nPoznámka: {notes}" if notes else "" msg = ( f"Přepnutí provozního režimu – lokalita `{site_code}`\n" f"**{previous_mode}** → **{new_mode}**\n" f"Aktivoval: `{activated_by}`{note_line}" ) await send_discord(msg, level=lvl) async def run_fn_set_mode_with_discord( conn: asyncpg.Connection, site_id: int, mode_code: str, activated_by: str, valid_until: datetime | None, notes: str | None, *, notify_level: str | None = None, ) -> str: """ Zavolá ems.fn_set_mode. Při skutečné změně režimu pošle Discord (pokud je webhook). Vrátí aktuální mode_code z DB po volání. """ prev = await conn.fetchval( "SELECT mode_code FROM ems.site_operating_mode WHERE site_id = $1", site_id, ) await conn.execute( "SELECT ems.fn_set_mode($1, $2, $3, $4, $5)", site_id, mode_code, activated_by, valid_until, notes, ) new = await conn.fetchval( "SELECT mode_code FROM ems.site_operating_mode WHERE site_id = $1", site_id, ) if new is None: new = mode_code if prev is not None and prev != new: site_code = await conn.fetchval( "SELECT code FROM ems.site WHERE id = $1", site_id ) await notify_operating_mode_changed( site_code or str(site_id), str(prev), str(new), activated_by, notes, level=notify_level, ) return str(new) async def send_discord(message: str, level: str = "info") -> bool: """ Pošle notifikaci na Discord webhook. level: 'info', 'warning', 'error', 'critical' Vrátí True při úspěchu. """ settings = get_settings() webhook_url = settings.discord_webhook_url if not webhook_url: logger.debug("Discord webhook not configured, skipping notification") return False emoji = {"info": "ℹ️", "warning": "⚠️", "error": "❌", "critical": "🚨"}.get(level, "ℹ️") try: async with httpx.AsyncClient(timeout=10) as client: resp = await client.post( webhook_url, json={ "content": f"{emoji} **EMS Alert** [{level.upper()}]\n{message}", }, ) resp.raise_for_status() return True except Exception as e: logger.warning("Discord notification failed: %s", e) return False async def notify_modbus_mismatch( asset_code: str, register: int, register_name: str, value_written: int, value_verified: int, attempt: int, ) -> None: msg = ( f"Modbus mismatch na **{asset_code}**\n" f"Registr: `0x{register:04X}` ({register_name})\n" f"Zapsáno: `{value_written}` | Přečteno: `{value_verified}`\n" f"Pokus č. {attempt}" ) await send_discord(msg, level="error") async def notify_self_sustain_activated(site_code: str, reason: str) -> None: msg = ( f"Přepnutí na **SELF_SUSTAIN** – lokalita `{site_code}`\n" f"Důvod: {reason}" ) await send_discord(msg, level="critical") async def notify_modbus_clock_verify_exhausted( site_code: str, asset_code: str, written: tuple[int, int, int], actual: tuple[int, int, int], ) -> None: msg = ( f"Modbus **systémový čas 62–64** – po 3 neúspěšných ověřeních **bez** přepnutí režimu.\n" f"Lokalita `{site_code}`, zařízení `{asset_code}`\n" f"Zapsáno: `{written}` | Přečteno: `{actual}`\n" f"Doporučení: zkontrolovat firmware/RS485; režim EMS se nemění automaticky." ) await send_discord(msg, level="critical") async def notify_daily_economics( site_code: str, day: str, import_kwh: float, import_cost: float, export_kwh: float, export_revenue: float, green_bonus: float, total_balance: float, planned_balance: float | None, ) -> None: lines = [ f"Ekonomika **{site_code}** {day}:", f" Import: {import_kwh:.1f} kWh = {import_cost:.2f} Kč", f" Export: {export_kwh:.1f} kWh = {export_revenue:.2f} Kč", ] if green_bonus > 0: lines.append(f" Zelený bonus: {green_bonus:.2f} Kč") sign = "+" if total_balance >= 0 else "" lines.append(f" **BILANCE: {sign}{total_balance:.2f} Kč**") if planned_balance is not None: dev = total_balance - planned_balance dev_sign = "+" if dev >= 0 else "" lines.append( f" Plán předpokládal: {planned_balance:+.2f} Kč " f"(odchylka {dev_sign}{dev:.2f} Kč)" ) await send_discord("\n".join(lines), level="info")