feat(nina): Quellenkennung, Schalter nebeneinander, Min.-Intervalle, last_sent (closes #2)
- _format_alert: Präfix [SOURCE@NINA] je nach Quelle - _send(): _last_sent bei Mesh-Versand setzen - get_config(): last_sent zurückgeben - nina.html: Toggles nebeneinander (d-flex gap-4), Intervall-Labels in Min., lastSent-Element ergänzt - nina.js: applyConfig ÷60 / Speichern ×60, lastSent befüllen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
76f04105b7
commit
07676a8c96
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -1,5 +1,19 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.08.22] - 2026-02-20
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **NINA Quellenkennung** (closes #2 Aufgabe 1): Präfix `[NINA]` → `[DWD@NINA]`,
|
||||||
|
`[KATWARN@NINA]` usw. je nach Warnquelle.
|
||||||
|
- **NINA Schalter nebeneinander** (closes #2 Aufgabe 2): "Aktiviert" und
|
||||||
|
"Ins Mesh senden" liegen jetzt in einer Zeile nebeneinander.
|
||||||
|
- **NINA Intervalle in Minuten** (closes #2 Aufgabe 3): UI zeigt und speichert
|
||||||
|
Abfrage- und Wiederholungsintervall in Minuten (intern weiterhin Sekunden).
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **NINA Zuletzt gesendet** (closes #2 Aufgabe 4): Neues Feld unter dem
|
||||||
|
Abfrageintervall zeigt, wann zuletzt eine Meldung ins Mesh gesendet wurde.
|
||||||
|
|
||||||
## [0.08.21] - 2026-02-20
|
## [0.08.21] - 2026-02-20
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
version: "0.08.21"
|
version: "0.08.22"
|
||||||
|
|
||||||
bot:
|
bot:
|
||||||
name: "MeshDD-Bot"
|
name: "MeshDD-Bot"
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,7 @@ class NinaBot:
|
||||||
self._task: asyncio.Task | None = None
|
self._task: asyncio.Task | None = None
|
||||||
self._resend_task: asyncio.Task | None = None
|
self._resend_task: asyncio.Task | None = None
|
||||||
self._last_poll: str = ""
|
self._last_poll: str = ""
|
||||||
|
self._last_sent: str = ""
|
||||||
self._load()
|
self._load()
|
||||||
|
|
||||||
# ── Config ──────────────────────────────────────────────────────────────
|
# ── Config ──────────────────────────────────────────────────────────────
|
||||||
|
|
@ -154,7 +155,7 @@ class NinaBot:
|
||||||
logger.exception("Error saving nina.yaml")
|
logger.exception("Error saving nina.yaml")
|
||||||
|
|
||||||
def get_config(self) -> dict:
|
def get_config(self) -> dict:
|
||||||
return {**self.config, "last_poll": self._last_poll}
|
return {**self.config, "last_poll": self._last_poll, "last_sent": self._last_sent}
|
||||||
|
|
||||||
def get_active_alerts(self) -> list[dict]:
|
def get_active_alerts(self) -> list[dict]:
|
||||||
return sorted(self._active.values(), key=lambda x: x.get("sent", ""), reverse=True)
|
return sorted(self._active.values(), key=lambda x: x.get("sent", ""), reverse=True)
|
||||||
|
|
@ -351,7 +352,7 @@ class NinaBot:
|
||||||
description = data.get("description", "")
|
description = data.get("description", "")
|
||||||
area = AGS_LABELS.get(ags.ljust(12, "0"), ags)
|
area = AGS_LABELS.get(ags.ljust(12, "0"), ags)
|
||||||
|
|
||||||
text = self._format_alert(msg_type, severity, headline, description, area)
|
text = self._format_alert(msg_type, severity, headline, description, area, src_key or "")
|
||||||
logger.info("NINA dashboard alert: %s (id=%s, area=%s)", headline, identifier, area)
|
logger.info("NINA dashboard alert: %s (id=%s, area=%s)", headline, identifier, area)
|
||||||
await self._send(identifier, severity, msg_type, headline, sent, text, channel, area)
|
await self._send(identifier, severity, msg_type, headline, sent, text, channel, area)
|
||||||
|
|
||||||
|
|
@ -379,11 +380,11 @@ class NinaBot:
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
try:
|
try:
|
||||||
await self._process_map_item(item, min_level, channel)
|
await self._process_map_item(item, min_level, channel, source_key)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("NINA mapData: error processing %s", item.get("id"))
|
logger.exception("NINA mapData: error processing %s", item.get("id"))
|
||||||
|
|
||||||
async def _process_map_item(self, item: dict, min_level: int, channel: int):
|
async def _process_map_item(self, item: dict, min_level: int, channel: int, source_key: str = ""):
|
||||||
identifier = item.get("id", "")
|
identifier = item.get("id", "")
|
||||||
if not identifier:
|
if not identifier:
|
||||||
return
|
return
|
||||||
|
|
@ -405,19 +406,20 @@ class NinaBot:
|
||||||
i18n = item.get("i18nTitle", {})
|
i18n = item.get("i18nTitle", {})
|
||||||
headline = i18n.get("de") or i18n.get("en") or identifier
|
headline = i18n.get("de") or i18n.get("en") or identifier
|
||||||
|
|
||||||
text = self._format_alert(msg_type, severity, headline, "")
|
text = self._format_alert(msg_type, severity, headline, "", "", source_key)
|
||||||
logger.info("NINA mapData alert: %s (id=%s)", headline, identifier)
|
logger.info("NINA mapData alert: %s (id=%s)", headline, identifier)
|
||||||
await self._send(identifier, severity, msg_type, headline, sent, text, channel, "")
|
await self._send(identifier, severity, msg_type, headline, sent, text, channel, "")
|
||||||
|
|
||||||
# ── Shared helpers ───────────────────────────────────────────────────────
|
# ── Shared helpers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _format_alert(msg_type: str, severity: str, headline: str, description: str, area: str = "") -> str:
|
def _format_alert(msg_type: str, severity: str, headline: str, description: str, area: str = "", source: str = "") -> str:
|
||||||
|
prefix = f"[{source.upper()}@NINA]" if source else "[NINA]"
|
||||||
area_suffix = f" ({area})" if area else ""
|
area_suffix = f" ({area})" if area else ""
|
||||||
if msg_type == "Cancel":
|
if msg_type == "Cancel":
|
||||||
return f"[NINA] Aufgehoben: {headline}{area_suffix}"
|
return f"{prefix} Aufgehoben: {headline}{area_suffix}"
|
||||||
sev_text = SEVERITY_LABELS.get(severity, severity)
|
sev_text = SEVERITY_LABELS.get(severity, severity)
|
||||||
text = f"[NINA] {sev_text}: {headline}{area_suffix}"
|
text = f"{prefix} {sev_text}: {headline}{area_suffix}"
|
||||||
if description:
|
if description:
|
||||||
short = description.strip()[:120]
|
short = description.strip()[:120]
|
||||||
if len(description.strip()) > 120:
|
if len(description.strip()) > 120:
|
||||||
|
|
@ -456,6 +458,7 @@ class NinaBot:
|
||||||
|
|
||||||
if self.config.get("send_to_mesh", True):
|
if self.config.get("send_to_mesh", True):
|
||||||
await self.send_callback(text, channel)
|
await self.send_callback(text, channel)
|
||||||
|
self._last_sent = datetime.now(timezone.utc).isoformat()
|
||||||
else:
|
else:
|
||||||
logger.info("NINA monitor-only: Mesh-Versand deaktiviert, nur WebSocket-Broadcast")
|
logger.info("NINA monitor-only: Mesh-Versand deaktiviert, nur WebSocket-Broadcast")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,8 +105,8 @@ async function loadConfig() {
|
||||||
function applyConfig(cfg) {
|
function applyConfig(cfg) {
|
||||||
document.getElementById('ninaEnabled').checked = !!cfg.enabled;
|
document.getElementById('ninaEnabled').checked = !!cfg.enabled;
|
||||||
document.getElementById('ninaSendToMesh').checked = cfg.send_to_mesh !== false;
|
document.getElementById('ninaSendToMesh').checked = cfg.send_to_mesh !== false;
|
||||||
document.getElementById('pollInterval').value = cfg.poll_interval ?? 300;
|
document.getElementById('pollInterval').value = Math.round((cfg.poll_interval ?? 300) / 60);
|
||||||
document.getElementById('resendInterval').value = cfg.resend_interval ?? 3600;
|
document.getElementById('resendInterval').value = Math.round((cfg.resend_interval ?? 3600) / 60);
|
||||||
document.getElementById('ninaChannel').value = cfg.channel ?? 0;
|
document.getElementById('ninaChannel').value = cfg.channel ?? 0;
|
||||||
document.getElementById('minSeverity').value = cfg.min_severity ?? 'Severe';
|
document.getElementById('minSeverity').value = cfg.min_severity ?? 'Severe';
|
||||||
|
|
||||||
|
|
@ -123,12 +123,15 @@ function applyConfig(cfg) {
|
||||||
|
|
||||||
const lpEl = document.getElementById('lastPoll');
|
const lpEl = document.getElementById('lastPoll');
|
||||||
if (lpEl) {
|
if (lpEl) {
|
||||||
if (cfg.last_poll) {
|
lpEl.textContent = cfg.last_poll
|
||||||
const d = new Date(cfg.last_poll);
|
? 'Letzte Abfrage: ' + new Date(cfg.last_poll).toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'medium' })
|
||||||
lpEl.textContent = 'Letzte Abfrage: ' + d.toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'medium' });
|
: '';
|
||||||
} else {
|
}
|
||||||
lpEl.textContent = '';
|
const lsEl = document.getElementById('lastSent');
|
||||||
}
|
if (lsEl) {
|
||||||
|
lsEl.textContent = cfg.last_sent
|
||||||
|
? 'Zuletzt gesendet: ' + new Date(cfg.last_sent).toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'medium' })
|
||||||
|
: '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,8 +154,8 @@ document.getElementById('btnSaveNina').addEventListener('click', async () => {
|
||||||
const payload = {
|
const payload = {
|
||||||
enabled: document.getElementById('ninaEnabled').checked,
|
enabled: document.getElementById('ninaEnabled').checked,
|
||||||
send_to_mesh: document.getElementById('ninaSendToMesh').checked,
|
send_to_mesh: document.getElementById('ninaSendToMesh').checked,
|
||||||
poll_interval: parseInt(document.getElementById('pollInterval').value) || 300,
|
poll_interval: (parseInt(document.getElementById('pollInterval').value) || 5) * 60,
|
||||||
resend_interval: parseInt(document.getElementById('resendInterval').value) || 3600,
|
resend_interval: (parseInt(document.getElementById('resendInterval').value) || 60) * 60,
|
||||||
channel: parseInt(document.getElementById('ninaChannel').value) || 0,
|
channel: parseInt(document.getElementById('ninaChannel').value) || 0,
|
||||||
min_severity: document.getElementById('minSeverity').value,
|
min_severity: document.getElementById('minSeverity').value,
|
||||||
ags_codes: [...agsCodes],
|
ags_codes: [...agsCodes],
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,8 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="ninaForm">
|
<form id="ninaForm">
|
||||||
<!-- Enable + send toggles -->
|
<!-- Enable + send toggles -->
|
||||||
<div class="mb-3">
|
<div class="mb-3 d-flex gap-4 flex-wrap">
|
||||||
<div class="form-check form-switch mb-2">
|
<div class="form-check form-switch">
|
||||||
<input class="form-check-input" type="checkbox" id="ninaEnabled" role="switch">
|
<input class="form-check-input" type="checkbox" id="ninaEnabled" role="switch">
|
||||||
<label class="form-check-label fw-semibold" for="ninaEnabled">Aktiviert</label>
|
<label class="form-check-label fw-semibold" for="ninaEnabled">Aktiviert</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -72,15 +72,18 @@
|
||||||
|
|
||||||
<div class="row g-3 mb-3">
|
<div class="row g-3 mb-3">
|
||||||
<div class="col-5">
|
<div class="col-5">
|
||||||
<label for="pollInterval" class="form-label">Abfrage­intervall (Sek.)</label>
|
<label for="pollInterval" class="form-label">Abfrage­intervall (Min.)</label>
|
||||||
<input type="number" class="form-control form-control-sm" id="pollInterval"
|
<input type="number" class="form-control form-control-sm" id="pollInterval"
|
||||||
min="60" max="3600" step="60" value="300">
|
min="1" max="60" step="1" value="5">
|
||||||
<div class="form-text">Neue Warnmeldungen<span id="lastPoll" class="d-block text-body-secondary"></span></div>
|
<div class="form-text">Neue Warnmeldungen
|
||||||
|
<span id="lastPoll" class="d-block text-body-secondary"></span>
|
||||||
|
<span id="lastSent" class="d-block text-body-secondary"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-5">
|
<div class="col-5">
|
||||||
<label for="resendInterval" class="form-label">Wieder­holungsintervall (Sek.)</label>
|
<label for="resendInterval" class="form-label">Wieder­holungsintervall (Min.)</label>
|
||||||
<input type="number" class="form-control form-control-sm" id="resendInterval"
|
<input type="number" class="form-control form-control-sm" id="resendInterval"
|
||||||
min="60" step="60" value="3600">
|
min="1" step="1" value="60">
|
||||||
<div class="form-text">Aktive Warnungen</div>
|
<div class="form-text">Aktive Warnungen</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue