Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 33 additions & 20 deletions docu/docs/plugin/telegram.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
---

## Beschreibung
Mit diesem Plugin ist es moeglich, Telegram-Nachrichten für POCSAG-Alarmierungen zu senden.
Außerdem werden Locations versendet, wenn die Felder `lat` und `lon` im Paket definiert sind. (beispielsweise durch das [Geocoding](../modul/geocoding.md) Modul)
Dieses Plugin ermöglicht das Versenden von Telegram-Nachrichten für verschiedene Alarmierungsarten.
Wenn im eingehenden Paket die Felder `lat` und `lon` vorhanden sind (z. B. durch das [Geocoding](../modul/geocoding.md) Modul), wird zusätzlich automatisch der Standort als Telegram-Location gesendet.

Die abarbeitung der Alarmierungen erfolgt per Queue nach den Limits der Telegram API, damit keine Nachrichten verloren gehen, diese Funktion kann mit dem ```queue``` Parameter deaktiviert werden.
Das Senden der Nachrichten erfolgt über eine interne Queue mit Retry-Logik und exponentiellem Backoff, um die Vorgaben der Telegram API einzuhalten und Nachrichtenverluste zu verhindern. Die Retry-Parameter (max_retries, initial_delay, max_delay) können in der Konfiguration angepasst werden.

## Unterstütze Alarmtypen
- Fms
- Pocsag
- Zvei
- Msg
- FMS
- POCSAG
- ZVEI
- MSG

## Resource
`telegram`
Expand All @@ -20,37 +20,50 @@ Die abarbeitung der Alarmierungen erfolgt per Queue nach den Limits der Telegram

|Feld|Beschreibung|Default|
|----|------------|-------|
|botToken|Der Api-Key des Telegram-Bots||
|chatIds|Liste mit Chat-Ids der Empfängers / der Emfänger-Gruppen||
|startup_message|Nachricht, dass das Telegram-Plugin erfolgreich geladen wurde|leer|
|message_fms|Format der Nachricht für FMS|`{FMS}`|
|message_pocsag|Format der Nachricht für Pocsag|`{RIC}({SRIC})\n{MSG}`|
|message_zvei|Format der Nachricht für ZVEI|`{TONE}`|
|message_msg|Format der Nachricht für MSG||
|max_retries|Anzahl der Versuche, bis das Senden abgebrochen wird|5|
|initial_delay|Verzögerung des zweiten Sendeversuchs|2 [Sek.]|
|max_delay|Maximale Verzögerung|60 [Sek.]|
|botToken|Der Api-Key des Telegram-Bots|-|
|chatIds|Liste mit Chat-Ids der Empfängers / der Empfänger-Gruppen|-|
|startup_message|Nachricht beim erfolgreichen Initialisieren des Plugins|leer|
|message_fms|Formatvorlage für FMS-Alarm|`{FMS}`|
|message_pocsag|Formatvorlage für POCSAG|`{RIC}({SRIC})\n{MSG}`|
|message_zvei|Formatvorlage für ZVEI|`{TONE}`|
|message_msg|Formatvorlage für MSG-Nachricht|-|
|max_retries|Anzahl Wiederholungsversuche bei Fehlern|5|
|initial_delay|Initiale Wartezeit bei Wiederholungsversuchen|2 [Sek.]|
|max_delay|Maximale Retry-Verzögerung|300 [Sek.]|
|parse_mode|Formatierung ("HTML" oder "MarkdownV2"), Case-sensitive!|leer|

**Beispiel:**
```yaml
- type: plugin
name: Telegram Plugin
res: telegram
config:
message_pocsag: "{RIC}({SRIC})\n{MSG}"
message_pocsag: |
<b>POCSAG Alarm:</b>
RIC: <b>{RIC}</b> ({SRIC})
{MSG}
parse_mode: "HTML"
startup_message: "Server up and running!"
botToken: "BOT_TOKEN"
chatIds:
- "CHAT_ID"
```

Hinweis:
Über parse_mode kannst du Telegram-Formatierungen verwenden:

- HTML: `<b>fett</b>`, `<i>kursiv</i>`, `<u>unterstrichen</u>`, `<s>durchgestrichen</s>`, ...
- MarkdownV2: `**fett**`, `__unterstrichen__`, `_italic \*text_` usw. (Escape-Regeln beachten)

Block-Strings (|) eignen sich perfekt für mehrzeilige Nachrichten und vermeiden Escape-Zeichen wie \n

---
## Modul Abhängigkeiten
Aus dem Modul [Geocoding](../modul/geocoding.md) (optional/nur POCSAG):
OPTIONAL, nur für POCSAG-Locationversand: Aus dem Modul [Geocoding](../modul/geocoding.md):

- `lat`
- `lon`

---
## Externe Abhängigkeiten
- python-telegram-bot
keine
33 changes: 19 additions & 14 deletions plugin/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
by Bastian Schroll

@file: telegram.py
@date: 12.07.2025
@date: 17.11.2025
@author: Claus Schichl nach der Idee von Jan Speller
@description: Telegram-Plugin mit Retry-Logik ohne externe Telegram-Abhängigkeiten
"""
Expand All @@ -31,16 +31,17 @@


# ===========================
# TelegramSender-Klasse
# TelegramSender-Class
# ===========================

class TelegramSender:
def __init__(self, bot_token, chat_ids, max_retries=None, initial_delay=None, max_delay=None):
def __init__(self, bot_token, chat_ids, max_retries=None, initial_delay=None, max_delay=None, parse_mode=None):
self.bot_token = bot_token
self.chat_ids = chat_ids
self.max_retries = max_retries if max_retries is not None else 5
self.initial_delay = initial_delay if initial_delay is not None else 2
self.max_delay = max_delay if max_delay is not None else 300
self.parse_mode = parse_mode
self.msg_queue = queue.Queue()
self._worker = threading.Thread(target=self._worker_loop, daemon=True)
self._worker.start()
Expand Down Expand Up @@ -75,11 +76,11 @@ def _worker_loop(self):
logger.warning(f"Erneutes Einreihen der Nachricht (Versuch {retry_count + 1}).")
self.msg_queue.put((msg_type, chat_id, content, retry_count + 1))

# Nutze den von Telegram gelieferten Wert (retry_after), falls vorhanden
# use the Telegram-provided value (retry_after) if available
wait_time = custom_delay if custom_delay is not None else delay
time.sleep(wait_time)

# Erhöhe Delay für den nächsten Versuch (exponentielles Backoff)
# increase delay for the next attempt (exponential backoff)
delay = min(delay * 2, self.max_delay)

except Exception as e:
Expand All @@ -91,8 +92,10 @@ def _send_to_telegram(self, msg_type, chat_id, content):
url = f"https://api.telegram.org/bot{self.bot_token}/sendMessage"
payload = {
'chat_id': chat_id,
'text': content
'text': content,
}
if self.parse_mode:
payload['parse_mode'] = self.parse_mode
elif msg_type == "location":
url = f"https://api.telegram.org/bot{self.bot_token}/sendLocation"
payload = {
Expand All @@ -101,25 +104,25 @@ def _send_to_telegram(self, msg_type, chat_id, content):
}
else:
logger.error("Unbekannter Nachrichtentyp.")
return False, True, None # Unbekannter Typ = permanent falsch
return False, True, None # unknown message type = permanent failure

try:
custom_delay = None # Standardwert für Rückgabe, außer bei 429
custom_delay = None # standardvalue for return, except in case of 429

response = requests.post(url, data=payload, timeout=10)

if response.status_code == 429:
custom_delay = response.json().get("parameters", {}).get("retry_after", 5)
logger.warning(f"Rate Limit erreicht – warte {custom_delay} Sekunden.")
return False, False, custom_delay # Telegram gibt genaue Wartezeit vor
return False, False, custom_delay # Telegram gives exact wait time

if response.status_code == 400:
logger.error("Ungültige Parameter – Nachricht wird nicht erneut gesendet.")
return False, True, custom_delay # Permanent fehlerhaft
return False, True, custom_delay # permanent failure

if response.status_code == 401:
logger.critical("Ungültiger Bot-Token – bitte prüfen!")
return False, True, custom_delay # Permanent fehlerhaft
return False, True, custom_delay # permanent failure

response.raise_for_status()
logger.info(f"Erfolgreich gesendet an Chat-ID {chat_id}")
Expand All @@ -131,7 +134,7 @@ def _send_to_telegram(self, msg_type, chat_id, content):


# ===========================
# BoswatchPlugin-Klasse
# BoswatchPlugin-Class
# ===========================


Expand All @@ -149,17 +152,19 @@ def onLoad(self):
logger.error("botToken oder chatIds fehlen in der Konfiguration!")
return

# Konfigurierbare Parameter mit Fallback-Defaults
# configurable parameters with fallback defaults
max_retries = self.config.get("max_retries")
initial_delay = self.config.get("initial_delay")
max_delay = self.config.get("max_delay")
parse_mode = self.config.get("parse_mode")

self.sender = TelegramSender(
bot_token=bot_token,
chat_ids=chat_ids,
max_retries=max_retries,
initial_delay=initial_delay,
max_delay=max_delay
max_delay=max_delay,
parse_mode=parse_mode
)

startup_message = self.config.get("startup_message")
Expand Down