extend webhook per site
This commit is contained in:
@@ -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 | Δ (act−plan): **{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} Kč "
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user