extend webhook per site
Some checks failed
CI and deploy / migration-check (push) Failing after 14s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-04-26 22:04:48 +02:00
parent 5f96a4cf01
commit 1d04790f28
6 changed files with 100 additions and 10 deletions

View File

@@ -698,6 +698,8 @@ async def _verify_deye_clock_written_bundle(
attempts = max(attempts, ac)
await notify_modbus_mismatch(
db,
site_id,
ac0,
62,
"system_time_62_64",
@@ -726,6 +728,8 @@ async def _verify_deye_clock_written_bundle(
)
site = await db.fetchrow("SELECT code FROM ems.site WHERE id=$1", site_id)
await notify_modbus_clock_verify_exhausted(
db,
site_id,
site["code"] if site else str(site_id),
ac0,
(w62, w63, w64),
@@ -858,6 +862,8 @@ async def verify_modbus_commands(
)
attempts = int(row_ac["attempt_count"] or 0) if row_ac else 0
await notify_modbus_mismatch(
db,
site_id,
cmd["asset_code"],
reg,
cmd["register_name"] or "",

View File

@@ -14,6 +14,40 @@ from app.config import get_settings
logger = logging.getLogger(__name__)
_WEBHOOK_CACHE: dict[tuple[int, str], str] = {}
async def _get_site_webhook_url(
conn: asyncpg.Connection | None,
site_id: int | None,
kind: str,
) -> str:
"""
kind: 'daily' | 'error'
Fallback: settings.discord_webhook_url
"""
settings = get_settings()
if site_id is None:
return settings.discord_webhook_url
cache_key = (int(site_id), str(kind))
cached = _WEBHOOK_CACHE.get(cache_key)
if cached is not None:
return cached
if conn is None:
return settings.discord_webhook_url
col = "discord_webhook_daily_url" if kind == "daily" else "discord_webhook_error_url"
try:
url = await conn.fetchval(
f"select {col} from ems.site where id = $1::int",
int(site_id),
)
except Exception:
logger.exception("Failed to load site webhook url site_id=%s kind=%s", site_id, kind)
url = None
final = str(url or settings.discord_webhook_url or "")
_WEBHOOK_CACHE[cache_key] = final
return final
def _discord_level_for_mode_change(activated_by: str) -> str:
if activated_by == "system:mismatch":
@@ -24,6 +58,8 @@ def _discord_level_for_mode_change(activated_by: str) -> str:
async def notify_operating_mode_changed(
conn: asyncpg.Connection | None,
site_id: int | None,
site_code: str,
previous_mode: str,
new_mode: str,
@@ -39,7 +75,7 @@ async def notify_operating_mode_changed(
f"**{previous_mode}** → **{new_mode}**\n"
f"Aktivoval: `{activated_by}`{note_line}"
)
await send_discord(msg, level=lvl)
await send_discord(conn, site_id, msg, level=lvl)
async def _auto_rolling_replan_after_self_sustain_exit(site_id: int) -> None:
@@ -100,6 +136,8 @@ async def run_fn_set_mode_with_discord(
site_code = ctx.get("site_code")
if prev is not None and prev != new:
await notify_operating_mode_changed(
conn,
site_id,
site_code or str(site_id),
str(prev),
str(new),
@@ -120,6 +158,8 @@ async def run_fn_set_mode_with_discord(
async def notify_plan_vs_actual_fatal(
conn: asyncpg.Connection | None,
site_id: int | None,
site_code: str,
slot_label: str,
interval_start_utc: datetime,
@@ -137,17 +177,22 @@ async def notify_plan_vs_actual_fatal(
f"**{reason_code}**: {detail}\n"
f"Plán grid: **{plan_grid_w}** W | Skutečnost: **{actual_grid_w}** W | Δ (actplan): **{deviation_grid_w}** W"
)
await send_discord(msg, level="critical")
await send_discord(conn, site_id, msg, level="critical")
async def send_discord(message: str, level: str = "info") -> bool:
async def send_discord(
conn: asyncpg.Connection | None,
site_id: int | None,
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
kind = "daily" if level == "info" else "error"
webhook_url = await _get_site_webhook_url(conn, site_id, kind)
if not webhook_url:
logger.debug("Discord webhook not configured, skipping notification")
return False
@@ -170,6 +215,8 @@ async def send_discord(message: str, level: str = "info") -> bool:
async def notify_modbus_mismatch(
conn: asyncpg.Connection | None,
site_id: int | None,
asset_code: str,
register: int,
register_name: str,
@@ -183,18 +230,25 @@ async def notify_modbus_mismatch(
f"Zapsáno: `{value_written}` | Přečteno: `{value_verified}`\n"
f"Pokus č. {attempt}"
)
await send_discord(msg, level="error")
await send_discord(conn, site_id, msg, level="error")
async def notify_self_sustain_activated(site_code: str, reason: str) -> None:
async def notify_self_sustain_activated(
conn: asyncpg.Connection | None,
site_id: int | None,
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")
await send_discord(conn, site_id, msg, level="critical")
async def notify_modbus_clock_verify_exhausted(
conn: asyncpg.Connection | None,
site_id: int | None,
site_code: str,
asset_code: str,
written: tuple[int, int, int],
@@ -206,10 +260,12 @@ async def notify_modbus_clock_verify_exhausted(
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")
await send_discord(conn, site_id, msg, level="critical")
async def notify_daily_economics(
conn: asyncpg.Connection | None,
site_id: int | None,
site_code: str,
day: str,
import_kwh: float,
@@ -236,4 +292,4 @@ async def notify_daily_economics(
f" Plán předpokládal: {planned_balance:+.2f}"
f"(odchylka {dev_sign}{dev:.2f} Kč)"
)
await send_discord("\n".join(lines), level="info")
await send_discord(conn, site_id, "\n".join(lines), level="info")

View File

@@ -46,6 +46,7 @@ async def _dispatch_site_result(site_payload: dict[str, Any]) -> None:
logger.warning("plan_actual_slot_guard: unknown site_id=%s", site_payload.get("site_id"))
return
site_code = str(site_payload.get("site_code") or site_payload.get("site_id") or "")
site_id = int(site_payload.get("site_id") or 0) or None
alerts = site_payload.get("alerts")
if not isinstance(alerts, list):
return
@@ -62,6 +63,8 @@ async def _dispatch_site_result(site_payload: dict[str, Any]) -> None:
deviation_grid_w = int(alert.get("deviation_grid_w") or 0)
slot_label = _slot_label_prague(interval_start)
await notify_plan_vs_actual_fatal(
None,
site_id,
site_code=site_code,
slot_label=slot_label,
interval_start_utc=interval_start,