Compare commits

...

7 commits

Author SHA1 Message Date
KoenigMjr dca8d9106c enh: CSV + Regex für descriptor-Modul
- Füge CSV-Import über csvPath-Konfiguration hinzu
- Implementiere Regex-Matching mit isRegex-Flag (YAML & CSV)
- Erstelle unified cache für YAML- und CSV-Einträge
- Wildcard-Replacement mit dynamische Beschreibungen
- Erweitere Logging für bessere Debugging-Möglichkeiten

Neue Features:
* CSV-Dateien können parallel zu YAML-Beschreibungen verwendet werden
* Regex-Unterstützung ermöglicht Pattern-basiertes Matching
* Wildcards wie {TONE} werden in Beschreibungen ("add"-Werte) ersetzt
* Vollständige Abwärtskompatibilität zu bestehenden Konfigurationen

Technische Verbesserungen:
* Unified cache-System für bessere Performance
* Korrekte Iteration über Config-Objekte mit default-Parametern
* Robuste Fehlerbehandlung für CSV-Import
* continue statt break bei fehlenden scanFields

Einschränkungen / known limitations:
* Keine explizite Behandlung von Duplikaten
* Standardverhalten ist „last one wins“, d. h. das zuletzt passende Descriptor-Objekt überschreibt den Wert
* Wenn mehrere CSV/YAML denselben Schlüssel liefern, hängt das Ergebnis von Lade- bzw. Listen-Reihenfolge ab
2025-10-22 09:59:11 +02:00
Bastian Schroll 017e882363
Merge pull request #134 from KoenigMjr/feature/telegram-neu
Some checks failed
build_docs / Build documentation (push) Has been cancelled
CodeQL / CodeQL-Build (push) Has been cancelled
pytest / build (ubuntu-latest, 3.10) (push) Has been cancelled
pytest / build (ubuntu-latest, 3.11) (push) Has been cancelled
pytest / build (ubuntu-latest, 3.12) (push) Has been cancelled
pytest / build (ubuntu-latest, 3.13) (push) Has been cancelled
pytest / build (ubuntu-latest, 3.9) (push) Has been cancelled
build_docs / deploy (push) Has been cancelled
Telegram-Plugin Refactor
2025-10-22 08:53:40 +02:00
Bastian Schroll 2e5479cde2
Merge branch 'develop' into feature/telegram-neu 2025-10-21 13:39:04 +02:00
Bastian Schroll 16dbd731e8
Merge pull request #136 from KoenigMjr/feature/service
Some checks are pending
build_docs / Build documentation (push) Waiting to run
build_docs / deploy (push) Blocked by required conditions
CodeQL / CodeQL-Build (push) Waiting to run
pytest / build (ubuntu-latest, 3.10) (push) Waiting to run
pytest / build (ubuntu-latest, 3.11) (push) Waiting to run
pytest / build (ubuntu-latest, 3.12) (push) Waiting to run
pytest / build (ubuntu-latest, 3.13) (push) Waiting to run
pytest / build (ubuntu-latest, 3.9) (push) Waiting to run
feat: Interaktives Installationsskript mit Mehrsprachigkeit, argparse und Logging
2025-10-21 13:38:50 +02:00
KoenigMjr cd21f07755 feat: Interaktives Installationsskript mit Mehrsprachigkeit, argparse und Logging
- Neues CLI-Interface via argparse für flexible Steuerung
- Unterstützt Dry-Run-Modus zur sicheren Vorschau
- Sprachumschaltung via --lang (de/en)
- Internationalisierung aller Ausgaben via `t()` und `TEXT`-Dict (Deutsch/Englisch)
- Logging mit farbiger Terminalausgabe und Logdatei (log/install/)
- YAML-Validierung und Service-Typ-Erkennung (client/server)
- Interaktive Benutzerführung für (De)Installation von Services
- Verbesserte Fehlerbehandlung und Nutzerabfragen mit Fallback
- DOKU:
- Install.md (Installation von BW3) ergänzt
- Service.md (für Install as a Service-Skript) zweisprachig (Deutsch/Englisch) ergänzt
- mkdocs um Seiten Install/Service/Usage.md ergänzt
2025-08-08 21:12:17 +02:00
KoenigMjr 523329a9bb Doku-Ergänzung
update zur neuen Telegram Version

*in Konfiguration hinzugefügt:*
Startup_message
max_retries
initial_delay
max_delay

*gelöscht:*
queue

*im Beispiel:*
Startup_message hinzugefügt
2025-08-08 21:11:48 +02:00
KoenigMjr 6a0a59c3ac Telegram mit Warteschlange
Durch Einbau einer Warteschlange kein Datenverlust bei belegter API (Sendelimit ca. 30 Nachrichten/min, gibt aber Soft-Limit)

Exponentielles Backoff mit Maximalgrenze
Retry-Zähler mit Abbruch bei zu vielen Fehlversuchen
Kein Wiederholen bei permanenten Fehlern (400/401)
dynamische Zeitanpassung bei 429 Fehlern

Fehlerrobustheit verbessert hinsichtlich Connection Error

neues Plugin ohne telegram-bot

* Timeout (timeout=10),
* HTTP-Fehlerprüfung (raise_for_status()),
* Retry-Logik (3 Versuche mit wachsender Wartezeit),
* Sauberem Logging mit logger statt print).

send_location aus altem Skript übernommen und angepasst
2025-08-08 21:11:48 +02:00
9 changed files with 1222 additions and 101 deletions

136
docu/docs/install.md Normal file
View file

@ -0,0 +1,136 @@
# 🇩🇪 Anleitung zur Installation von BOSWatch3
Die Installation von BOSWatch3 wird mittels diesem bash-Skript weitestgehend automatisiert durchgeführt.
## 1. Installationsskript herunterladen
Zunächst wird das aktuelle Installationsskript heruntergeladen
Öffne ein Terminal und führe folgenden Befehl aus:
```bash
wget https://github.com/BOSWatch/BW3-Core/raw/master/install.sh
```
## 2. Installationsskript ausführen
Im Anschluss wird das Skript mit dem Kommando
```bash
sudo bash install.sh
```
ausgeführt.
### 2a. Optionale Parameter beim Installieren
Standardmäßig wird das Programm nach /opt/boswatch3 installiert. Folgende Parameter stehen zur Installation zur Verfügung:
| Parameter | Zulässige Werte | Beschreibung |
| ---------------- | ----------------------- | --------------------------------------------------------------------------------------------------- |
| `-r`, `--reboot` | *(kein Wert notwendig)* | Führt nach der Installation automatisch einen Neustart durch. Ohne Angabe erfolgt **kein** Reboot. |
| `-b`, `--branch` | `master`, `develop` | Wählt den zu installierenden Branch. `master` ist stabil (empfohlen), `develop` ist für Entwickler. |
| `-p`, `--path` | z.B. `/opt/boswatch3` | Installiert BOSWatch3 in ein anderes Verzeichnis (**nicht empfohlen**). Standard ist `/opt/boswatch3`. |
**ACHTUNG:**
Eine Installation von BOSWatch3 in ein anderes Verzeichnis erfordert viele Anpassungen in den Skripten und erhöht das Risiko, dass das Programm zu Fehlern führt. Es wird dazu geraten, das Standardverzeichnis zu benutzen.
Falls eine Installation mit Parameter gewünscht wird, so kann dies wie in folgendem Beispiel gestartet werden:
```bash
sudo bash install.sh --branch master --path /opt/boswatch3 --reboot
```
## 3. Konfiguration nach der Installation
Nach der Installation muss die Konfiguration der Dateien `/opt/boswatch3/config/client.yaml`
und `/opt/boswatch3/config/server.yaml` angepasst werden (z.B. mit nano, WinSCP,...):
```bash
sudo nano /opt/boswatch3/config/client.yaml
```
und
```bash
sudo nano /opt/boswatch3/config/server.yaml
```
Passe die Einstellungen nach deinen Anforderungen an. Bei einem Upgrade einer bestehenden Version kann dieser Schritt ggf. entfallen.
**INFORMATION:**
Weitere Informationen zur Konfiguration:
[Konfiguration](/docu/docs/config.md)
## 4. Neustart
**WICHTIG:**
Bitte starte das System neu, bevor du BOSWatch3 zum ersten Mal startest!
```bash
sudo reboot
```
## 5. Start von BOSWatch3
weiter gehts bei [BOSWatch benutzen](usage.md)
---
# 🇬🇧 BOSWatch3 Installation Guide
The installation of BOSWatch3 is largely automated using this bash script.
## 1. Download the Installation Script
First, download the latest installation script.
Open a terminal and run the following command:
```bash
wget https://github.com/BOSWatch/BW3-Core/raw/master/install.sh
```
## 2. Run the Installation Script
Then run the script with the command:
```bash
sudo bash install.sh
```
### 2a. Optional Parameters for Installation
By default, the program is installed to `/opt/boswatch3`. The following parameters are available for installation:
| Parameter | Allowed Values | Description |
| ---------------- | -----------------------| --------------------------------------------------------------------------------------------------- |
| `-r`, `--reboot` | *(no value needed)* | Automatically reboots the system after installation. Without this, **no** reboot will be performed. |
| `-b`, `--branch` | `master`, `develop` | Selects the branch to install. `master` is stable (recommended), `develop` is for developers. |
| `-p`, `--path` | e.g. `/opt/boswatch3` | Installs BOSWatch3 to a different directory (**not recommended**). Default is `/opt/boswatch3`. |
**WARNING:**
Installing BOSWatch3 to a different directory requires many adjustments in the scripts and increases the risk of errors. It is recommended to use the default directory.
If you want to install with parameters, you can run the following example command:
```bash
sudo bash install.sh --branch master --path /opt/boswatch3 --reboot
```
## 3. Configuration After Installation
After installation, the configuration files `/opt/boswatch3/config/client.yaml`
and `/opt/boswatch3/config/server.yaml` must be adjusted (e.g. using nano, WinSCP, ...):
```bash
sudo nano /opt/boswatch3/config/client.yaml
```
and
```bash
sudo nano /opt/boswatch3/config/server.yaml
```
Adjust the settings according to your requirements. If upgrading from an existing version, this step might be skipped.
**INFORMATION:**
More information about configuration:
[Configuration](/docu/docs/config.md)
## 4. Reboot
**IMPORTANT:**
Please reboot the system before starting BOSWatch3 for the first time!
```bash
sudo reboot
```
## 5. Starting BOSWatch3
Continue with [Using BOSWatch](usage.md)

View file

@ -24,12 +24,14 @@ Informationen zum Aufbau eines [BOSWatch Pakets](../develop/packet.md)
|descrField|Name des Feldes im BW Paket in welchem die Beschreibung gespeichert werden soll||
|wildcard|Optional: Es kann für das angelegte `descrField` automatisch ein Wildcard registriert werden|None|
|descriptions|Liste der Beschreibungen||
|csvPath|Pfad der CSV-Datei (relativ zum Projektverzeichnis)||
#### `descriptions:`
|Feld|Beschreibung|Default|
|----|------------|-------|
|for|Inhalt im `scanField` auf welchem geprüft werden soll||
|add|Beschreibungstext welcher im `descrField` hinterlegt werden soll||
|isRegex|Muss explizit auf `true` gesetzt werden, falls RegEx verwendet wird|false|
**Beispiel:**
```yaml
@ -44,6 +46,9 @@ Informationen zum Aufbau eines [BOSWatch Pakets](../develop/packet.md)
add: FF DescriptorTest
- for: '05678' # führende Nullen in '' !
add: FF TestDescription
- for: '890(1[1-9]|2[0-9])' # Regex-Pattern in '' !
add: Feuerwehr Wache \\1 (BF)
isRegex: true
- scanField: status
descrField: fmsStatDescr
wildcard: "{STATUSTEXT}"
@ -55,6 +60,62 @@ Informationen zum Aufbau eines [BOSWatch Pakets](../develop/packet.md)
- ...
```
**Wichtige Punkte für YAML-Regex:**
- Apostroph: Regex-Pattern sollten in `'` stehen, um YAML-Parsing-Probleme zu vermeiden
- isRegex-Flag: Muss explizit auf `true` gesetzt werden
- Escaping: Backslashes müssen in YAML doppelt escaped werden (`\\1` statt `\1`)
- Regex-Gruppen: Mit `\\1`, `\\2` etc. können Teile des Matches in der Beschreibung verwendet werden
#### `csvPath:`
**Beispiel:**
```
- type: module
res: descriptor
config:
- scanField: tone
descrField: description
wildcard: "{DESCR}"
csvPath: "config/descriptions_tone.csv"
```
`csvPath` gibt den Pfad zu einer CSV-Datei an, relativ zum Projektverzeichnis (z. B. `"config/descriptions_tone.csv"`).
Eine neue CSV-Datei (z. B. `descriptions_tone.csv`) hat folgendes Format:
**Beispiel**
```
for,add,isRegex
11111,KBI Landkreis Z,false
12345,FF A-Dorf,false
23456,FF B-Dorf,false
^3456[0-9]$,FF Grossdorf, true
```
In der Spalte isRegex kann **zusätzlich** angegeben werden, ob der Wert in for als regulärer Ausdruck interpretiert werden soll (true/false). Standardmäßig ist `false`.
Wenn `isRegex` auf `true` gesetzt ist, wird der Wert aus `for` als regulärer Ausdruck ausgewertet.
### Kombination von YAML- und CSV-Konfiguration
Beide Varianten können parallel genutzt werden. In diesem Fall werden zuerst die Beschreibungen aus der YAML-Konfiguration und zusätzlich die Beschreibungen aus der angegebenen CSV-Datei geladen.
**Beispiel**
```
- type: module
res: descriptor
config:
- scanField: tone
descrField: description
wildcard: "{DESCR}"
descriptions:
- for: 12345
add: FF YAML-Test
- for: '05678' # führende Nullen in '' !
add: FF YAML-Nullen
csvPath: "config/descriptions_tone.csv"
```
---
## Modul Abhängigkeiten
- keine

View file

@ -22,11 +22,14 @@ Die abarbeitung der Alarmierungen erfolgt per Queue nach den Limits der Telegram
|----|------------|-------|
|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||
|queue|Aktivieren/Deaktivieren der MessageQueue|true|
|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.]|
**Beispiel:**
```yaml
@ -35,6 +38,7 @@ Die abarbeitung der Alarmierungen erfolgt per Queue nach den Limits der Telegram
res: telegram
config:
message_pocsag: "{RIC}({SRIC})\n{MSG}"
startup_message: "Server up and running!"
botToken: "BOT_TOKEN"
chatIds:
- "CHAT_ID"

166
docu/docs/service.md Normal file
View file

@ -0,0 +1,166 @@
# BOSWatch Dienstinstallation (Service Setup)
## 🇩🇪 BOSWatch als Dienst verwenden
Es wird vorausgesetzt, dass BOSWatch unter `/opt/boswatch3` installiert ist.
Falls du einen anderen Installationspfad nutzt, müssen alle Pfadangaben in dieser Anleitung, im Skript sowie in den generierten Service-Dateien entsprechend angepasst werden.
Für jeden Dienst muss eine eigene `*.yaml`-Datei im Ordner `config/` vorhanden sein.
Beim Ausführen des Skripts wirst du interaktiv gefragt, welche dieser YAML-Dateien installiert oder übersprungen werden sollen.
### Dienst installieren
Als Erstes wechseln wir ins BOSWatch Verzeichnis:
```bash
cd /opt/boswatch3
```
Das Installationsskript `install_service.py` wird anschließend mit Root-Rechten ausgeführt:
```bash
sudo python3 install_service.py
```
Es folgt ein interaktiver Ablauf, bei dem du gefragt wirst, welche YAML-Dateien installiert oder entfernt werden sollen.
### Zusätzliche Optionen (fortgeschrittene Anwender)
Das Skript bietet zusätzliche CLI-Optionen für mehr Kontrolle:
```bash
usage: install_service.py [-h] [--verbose] [--quiet]
Installiert oder entfernt systemd-Services für BOSWatch basierend auf YAML-Konfigurationsdateien.
optional arguments:
-h, --help zeigt diese Hilfe an
--dry-run für Entwickler: führt keine echten Änderungen aus (Simulation)
--verbose zeigt ausführliche Debug-Ausgaben
--quiet unterdrückt alle Ausgaben außer Warnungen und Fehlern
-l, --lang [de|en] Sprache für alle Ausgaben (Standard: de)
```
### Neustart nach Serviceinstallation
Nach Durchlaufen des Skripts boote dein System erneut durch, um den korrekten Startvorgang zu überprüfen:
```bash
sudo reboot
```
### Kontrolle, ob alles funktioniert hat
Um zu kontrollieren, ob alles ordnungsgemäß hochgefahren ist, kannst du die zwei Services mit folgenden Befehlen abfragen und die letzten Log-Einträge ansehen:
1. Client-Service
```bash
sudo systemctl status bw3_[clientname].service
```
Ersetze [clientname] mit dem Namen, den deine client.yaml hat (Standardmäßig: client)
Um das Log zu schließen, "q" drücken.
2. Server-Service
```bash
sudo systemctl status bw3_[servername].service
```
Ersetze [servername] mit dem Namen, den deine server.yaml hat (Standardmäßig: server)
Um das Log zu schließen, "q" drücken.
**Beide Outputs sollten so ähnlich beginnen:**
```text
bw3_client.service - BOSWatch Client
Loaded: loaded (/etc/systemd/system/bw3_client.service; enabled; preset: enabled)
Active: active (running) since Mon 1971-01-01 01:01:01 CEST; 15min 53s ago
```
Falls du in deinen letzten Logzeilen keine Error vorfinden kannst, die auf einen Stopp des Clients bzw. Server hinweisen, läuft das Programm wie gewünscht, sobald du deinen Rechner startest.
### Logdatei
Alle Aktionen des Installationsskripts werden in der Datei log/install/service_install.log protokolliert.
### Hinweis
Nach der Installation oder Entfernung wird systemctl daemon-reexec automatisch aufgerufen, damit systemd die neuen oder entfernten Units korrekt verarbeitet.
---
## 🇬🇧 Use BOSWatch as a Service
We assume that BOSWatch is installed to `/opt/boswatch3`.
If you are using a different path, please adjust all paths in this guide, in the script and in the generated service files accordingly.
Each service requires its own `*.yaml` file inside the `config/` folder.
The script will interactively ask which YAML files to install or skip.
### Install the Service
First, change directory to BOSWatch folder:
```bash
cd /opt/boswatch3
```
After that, run the install script `install_service.py` with root permissions:
```bash
sudo python3 install_service.py -l en
```
You will be guided through an interactive selection to install or remove desired services.
### Additional Options
The script supports additional CLI arguments for advanced usage:
```bash
usage: install_service.py [-h] [--dry-run] [--verbose] [--quiet]
Installs or removes BOSWatch systemd services based on YAML config files.
optional arguments:
-h, --help show this help message and exit
--dry-run simulate actions without making real changes
--verbose show detailed debug output
--quiet suppress all output except warnings and errors
-l, --lang [de|en] Language for all output (default: de)
```
### Reboot After Setup
After running the script, reboot your system to verify that the services start correctly:
```bash
sudo reboot
```
### Verifying Successful Setup
To check if everything started properly, you can query the two services and inspect the most recent log entries:
1. Client Service
```bash
sudo systemctl status bw3_[clientname].service
```
Replace [clientname] with the name of your client.yaml file (default: client).
To close the log, press "q".
2. Server Service
```bash
sudo systemctl status bw3_[servername].service
```
Replace [servername] with the name of your server.yaml file (default: server).
To close the log, press "q".
**Both outputs should start similarly to this:**
```text
bw3_client.service - BOSWatch Client
Loaded: loaded (/etc/systemd/system/bw3_client.service; enabled; preset: enabled)
Active: active (running) since Mon 1971-01-01 01:01:01 CEST; 15min 53s ago
```
If the latest log entries do not show any errors indicating a client or server crash, then the services are running correctly and will automatically start on boot.
### Log File
All actions of the installation script are logged to `log/install/service_install.log`.
### Note
After installation or removal, `systemctl daemon-reexec` is automatically triggered to reload unit files properly.

25
docu/docs/usage.md Normal file
View file

@ -0,0 +1,25 @@
# 🇩🇪 Start von BOSWatch3
Nach dem Neustart kannst du BOSWatch3 wie folgt starten:
```bash
cd /opt/boswatch3
sudo python3 bw_client.py -c config/client.yaml
sudo python3 bw_server.py -c config/server.yaml
```
## Optional: Als Dienst einrichten
Weiter gehts mit [als Service einrichten](service.md)
---
# 🇬🇧 Starting BOSWatch3
After reboot, you can start BOSWatch3 as follows:
```bash
cd /opt/boswatch3
sudo python3 bw_client.py -c config/client.yaml
sudo python3 bw_server.py -c config/server.yaml
```
## Optional: Setup as a Service
For further instructions, see [Setup as a Service](service.md)

View file

@ -7,10 +7,10 @@ edit_uri: edit/develop/docu/docs/
nav:
# - BW3: index.md
- Quick Start:
- Installation: tbd.md
- Installation: install.md
- Konfiguration: config.md
# - BOSWatch benutzen: tbd.md
# - Als Service einrichten: tbd.md
- BOSWatch benutzen: usage.md
- Als Service einrichten: service.md
- Informationen:
- Server/Client Prinzip: information/serverclient.md
- Broadcast Service: information/broadcast.md

509
install_service.py Normal file
View file

@ -0,0 +1,509 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
r"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: http.py
@date: 21.07.2025
@author: Claus Schichl
@description: Install Service File with argparse CLI
"""
import os
import subprocess
import sys
import logging
import argparse
import yaml
from colorama import init as colorama_init, Fore, Style
from pathlib import Path
# === Initialisiere Colorama für Windows/Konsole ===
colorama_init(autoreset=True)
# === Konstanten ===
BASE_DIR = Path(__file__).resolve().parent
BW_DIR = '/opt/boswatch3'
SERVICE_DIR = Path('/etc/systemd/system')
CONFIG_DIR = (BASE_DIR / 'config').resolve()
LOG_FILE = (BASE_DIR / 'log' / 'install' / 'service_install.log').resolve()
os.makedirs(LOG_FILE.parent, exist_ok=True)
# === Sprache (kapselt globale Variable) ===
_lang = 'de'
def get_lang():
return _lang
def set_lang(lang):
global _lang
_lang = lang
# === Texte ===
TEXT = {
"de": {
"script_title": "🛠️ BOSWatch Service Manager",
"mode_dry": "DRY-RUN (nur Vorschau)",
"mode_live": "LIVE",
"no_yaml": "❌ Keine .yaml-Dateien im config-Verzeichnis gefunden.",
"found_yaml": "🔍 Gefundene YAML-Dateien: {}",
"action_prompt": "\nWas möchtest du tun? (i=installieren, r=entfernen, e=beenden): ",
"edited_prompt": "Wurden die YAML-Dateien korrekt bearbeitet? (y/n): ",
"edit_abort": "⚠️ Bitte YAML-Dateien zuerst bearbeiten. Vorgang abgebrochen.",
"install_confirm": "Service für '{}' installieren? (y/n): ",
"skip_invalid_yaml": "⏭ Überspringe fehlerhafte YAML: {}",
"install_done": "✅ Installation abgeschlossen. Services installiert: {}, übersprungen: {}",
"invalid_input": "Ungültige Eingabe. Erlaubt sind: {}",
"no_services": "Keine bw3-Services gefunden.",
"available_services": "\nVerfügbare bw3-Services:",
"remove_prompt": "Was soll deinstalliert werden? ",
"invalid_choice": "Ungültige Auswahl.",
"not_root": "🛑 Dieses Skript muss mit Root-Rechten ausgeführt werden (sudo).",
"help_dry_run": "Nur anzeigen, nicht ausführen",
"help_verbose": "Ausführliche Ausgabe",
"help_quiet": "Weniger Ausgabe",
"help_lang": "Sprache für alle Ausgaben [de/en] (Standard: de)",
"creating_service_file": "📄 Erstelle Service-Datei für {}{}",
"removing_service": "\n🗑 Entferne Service: {}",
"service_deleted": "{} gelöscht.",
"service_not_found": "{} nicht gefunden oder im Dry-Run-Modus.",
"yaml_error": "⚠ Fehler in YAML {}: {}",
"yaml_read_error": "⚠ Fehler beim Lesen der YAML-Datei {}: {}",
"unknown_yaml_type": "⚠ YAML-Typ für {} nicht erkannt. Service wird übersprungen.",
"verify_warn": "⚠ Warnung bei systemd-analyze verify:\n{}",
"verify_ok": "{} erfolgreich verifiziert.",
"install_skipped": "⏭ Installation für '{}' übersprungen",
"file_write_error": "⚠ Fehler beim Schreiben der Datei {}: {}",
"all": "[a] Alle deinstallieren",
"exit": "[e] Beenden",
"service_active": "✅ Service {0} läuft erfolgreich.",
"service_inactive": "⚠ Service {0} ist **nicht aktiv** bitte prüfen.",
"dryrun_status_check": "🧪 [Dry-Run] Service-Status von {0} würde jetzt geprüft.",
"max_attempts_exceeded": "❌ Maximale Anzahl an Eingabeversuchen überschritten. Das Menü wird beendet."
},
"en": {
"script_title": "🛠️ BOSWatch Service Manager",
"mode_dry": "DRY-RUN (preview only)",
"mode_live": "LIVE",
"no_yaml": "❌ No .yaml files found in config directory.",
"found_yaml": "🔍 YAML files found: {}",
"action_prompt": "\nWhat would you like to do? (i=install, r=remove, e=exit): ",
"edited_prompt": "Have the YAML files been edited correctly? (y/n): ",
"edit_abort": "⚠ Please edit the YAML files first. Aborting.",
"install_confirm": "Install service for '{}' ? (y/n): ",
"skip_invalid_yaml": "⏭ Skipping invalid YAML: {}",
"install_done": "✅ Installation complete. Services installed: {}, skipped: {}",
"invalid_input": "Invalid input. Allowed: {}",
"no_services": "No bw3 services found.",
"available_services": "\nAvailable bw3 services:",
"remove_prompt": "What should be removed? ",
"invalid_choice": "Invalid choice.",
"not_root": "🛑 This script must be run as root (sudo).",
"help_dry_run": "Show actions only, do not execute",
"help_verbose": "Show detailed output",
"help_quiet": "Reduce output verbosity",
"help_lang": "Language for all output [de/en] (default: de)",
"creating_service_file": "📄 Creating service file for {}{}",
"removing_service": "\n🗑 Removing service: {}",
"service_deleted": "{} deleted.",
"service_not_found": "{} not found or in dry-run mode.",
"yaml_error": "⚠ YAML error in {}: {}",
"yaml_read_error": "⚠ Error reading YAML file {}: {}",
"unknown_yaml_type": "⚠ Unknown YAML type for {}. Skipping service.",
"verify_warn": "⚠ Warning in systemd-analyze verify:\n{}",
"verify_ok": "{} verified successfully.",
"install_skipped": "⏭ Installation skipped for '{}'",
"file_write_error": "⚠ Error writing file {}: {}",
"all": "[a] Remove all",
"exit": "[e] Exit",
"service_active": "✅ Service {0} is running successfully.",
"service_inactive": "⚠ Service {0} is **not active** please check.",
"dryrun_status_check": "🧪 [Dry-Run] Service status of {0} would be checked now.",
"max_attempts_exceeded": "❌ Maximum number of input attempts exceeded. Exiting menu."
}
}
# === Logging Setup ===
def setup_logging(verbose=False, quiet=False):
"""
Setup logging to file and console with colorized output.
"""
log_level = logging.INFO
if quiet:
log_level = logging.WARNING
elif verbose:
log_level = logging.DEBUG
logger = logging.getLogger()
logger.setLevel(log_level)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
# File Handler (plain)
fh = logging.FileHandler(LOG_FILE)
fh.setFormatter(formatter)
logger.addHandler(fh)
# Console Handler (colorized)
class ColorFormatter(logging.Formatter):
COLORS = {
logging.DEBUG: Fore.CYAN,
logging.INFO: Fore.GREEN,
logging.WARNING: Fore.YELLOW,
logging.ERROR: Fore.RED,
logging.CRITICAL: Fore.RED + Style.BRIGHT,
}
def format(self, record):
color = self.COLORS.get(record.levelno, Fore.RESET)
message = super().format(record)
return f"{color}{message}{Style.RESET_ALL}"
ch = logging.StreamHandler(sys.stdout)
ch.setFormatter(ColorFormatter('%(levelname)s: %(message)s'))
logger.addHandler(ch)
return logger
def t(key):
"""
Translation helper: returns the localized string for the given key.
"""
lang = get_lang()
return TEXT.get(lang, TEXT['de']).get(key, key)
def get_user_input(prompt, valid_inputs, max_attempts=3):
"""
Prompt user for input until a valid input from valid_inputs is entered or max_attempts exceeded.
Raises RuntimeError on failure.
"""
attempts = 0
while attempts < max_attempts:
value = input(prompt).strip().lower()
if value in valid_inputs:
return value
logging.warning(t("invalid_input").format(", ".join(valid_inputs)))
attempts += 1
raise RuntimeError("Maximale Anzahl Eingabeversuche überschritten.")
def list_yaml_files():
"""
Returns a list of .yaml or .yml files in the config directory.
"""
return [f.name for f in CONFIG_DIR.glob("*.y*ml")]
def test_yaml_file(file_path):
"""
Tests if YAML file can be loaded without error.
"""
try:
content = file_path.read_text()
yaml.safe_load(content)
return True
except Exception as e:
logging.error(t("yaml_error").format(file_path, e))
return False
def detect_yaml_type(file_path):
"""
Detects if YAML config is 'client' or 'server' type.
"""
try:
with open(file_path, 'r') as f:
data = yaml.safe_load(f)
if 'client' in data:
return 'client'
elif 'server' in data:
return 'server'
else:
logging.error(t("unknown_yaml_type").format(os.path.basename(file_path)))
return None
except Exception as e:
logging.error(t("yaml_read_error").format(file_path, e))
return None
def execute(command, dry_run=False):
"""
Executes shell command unless dry_run is True.
"""
logging.debug(f"{command}")
if not dry_run:
subprocess.run(command, shell=True, check=False)
def verify_service(service_path):
"""
Runs 'systemd-analyze verify' on the service file and logs warnings/errors.
"""
try:
result = subprocess.run(['systemd-analyze', 'verify', service_path], capture_output=True, text=True)
if result.returncode != 0 or result.stderr:
logging.warning(t("verify_warn").format(result.stderr.strip()))
else:
logging.debug(t("verify_ok").format(os.path.basename(service_path)))
except Exception as e:
logging.error(t("yaml_error").format(service_path, e))
def install_service(yaml_file, dry_run=False):
"""
Creates and installs systemd service based on YAML config.
"""
yaml_path = CONFIG_DIR / yaml_file
yaml_type = detect_yaml_type(yaml_path)
if yaml_type == 'server':
is_server = True
elif yaml_type == 'client':
is_server = False
else:
logging.error(t("unknown_yaml_type").format(yaml_file))
return
service_name = f"bw3_{Path(yaml_file).stem}.service"
service_path = SERVICE_DIR / service_name
if is_server:
exec_line = f"/usr/bin/python {BW_DIR}/bw_server.py -c {yaml_file}"
description = "BOSWatch Server"
after = "network-online.target"
wants = "Wants=network-online.target"
else:
exec_line = f"/usr/bin/python3 {BW_DIR}/bw_client.py -c {yaml_file}"
description = "BOSWatch Client"
after = "network.target"
wants = ""
service_content = f"""[Unit]
Description={description}
After={after}
{wants}
[Service]
Type=simple
WorkingDirectory={BW_DIR}
ExecStart={exec_line}
Restart=on-abort
[Install]
WantedBy=multi-user.target
"""
logging.info(t("creating_service_file").format(yaml_file, service_name))
if not dry_run:
try:
with open(service_path, 'w') as f:
f.write(service_content)
except IOError as e:
logging.error(t("file_write_error").format(service_path, e))
return
verify_service(service_path)
execute("systemctl daemon-reexec", dry_run=dry_run)
execute(f"systemctl enable {service_name}", dry_run=dry_run)
execute(f"systemctl start {service_name}", dry_run=dry_run)
if not dry_run:
try:
subprocess.run(
["systemctl", "is-active", "--quiet", service_name],
check=True
)
logging.info(t("service_active").format(service_name))
except subprocess.CalledProcessError:
logging.warning(t("service_inactive").format(service_name))
else:
logging.info(t("dryrun_status_check").format(service_name))
def remove_service(service_name, dry_run=False):
"""
Stops, disables and removes the given systemd service.
"""
logging.warning(t("removing_service").format(service_name))
execute(f"systemctl stop {service_name}", dry_run=dry_run)
execute(f"systemctl disable {service_name}", dry_run=dry_run)
service_path = Path(SERVICE_DIR) / service_name
if not dry_run and service_path.exists():
try:
os.remove(service_path)
logging.info(t("service_deleted").format(service_name))
except Exception as e:
logging.error(t("file_write_error").format(service_path, e))
else:
logging.warning(t("service_not_found").format(service_name))
def remove_menu(dry_run=False):
"""
Interactive menu to remove services.
"""
while True:
services = sorted([
f for f in os.listdir(SERVICE_DIR)
if f.startswith('bw3_') and f.endswith('.service')
])
if not services:
print(Fore.YELLOW + t("no_services") + Style.RESET_ALL)
return
print(Fore.CYAN + "\n" + t("available_services") + Style.RESET_ALL)
for i, s in enumerate(services):
print(f" [{i}] {s}")
print(" " + t("all"))
print(" " + t("exit"))
try:
auswahl = get_user_input(
t("remove_prompt"),
['e', 'a'] + [str(i) for i in range(len(services))]
)
except RuntimeError:
logging.error(t("max_attempts_exceeded"))
break
if auswahl == 'e':
break
elif auswahl == 'a':
for s in services:
remove_service(s, dry_run=dry_run)
# Danach direkt weiter zur nächsten Schleife (aktualisierte Liste!)
continue
else:
remove_service(services[int(auswahl)], dry_run=dry_run)
# Danach ebenfalls weiter zur nächsten Schleife (aktualisierte Liste!)
continue
def init_language():
"""
Parses --lang/-l argument early to set language before other parsing.
"""
lang_parser = argparse.ArgumentParser(add_help=False)
lang_parser.add_argument(
'--lang', '-l',
choices=['de', 'en'],
default='de',
metavar='LANG',
help=TEXT["en"]["help_lang"]
)
lang_args, remaining_argv = lang_parser.parse_known_args()
set_lang(lang_args.lang)
return lang_parser, remaining_argv
def main(dry_run=False):
"""
Hauptprogramm: Service installieren oder entfernen.
"""
print(Fore.GREEN + Style.BRIGHT + t("script_title") + Style.RESET_ALL)
print(t('mode_dry') if dry_run else t('mode_live'))
print()
yaml_files = list_yaml_files()
if not yaml_files:
print(Fore.RED + t("no_yaml") + Style.RESET_ALL)
sys.exit(1)
print(Fore.GREEN + t("found_yaml").format(len(yaml_files)) + Style.RESET_ALL)
for f in yaml_files:
file_path = CONFIG_DIR / f
valid = test_yaml_file(file_path)
status = Fore.GREEN + "" if valid else Fore.RED + ""
print(f" - {f} {status}{Style.RESET_ALL}")
try:
action = get_user_input(t("action_prompt"), ['i', 'r', 'e'])
except RuntimeError:
logging.error("Maximale Anzahl Eingabeversuche überschritten. Beende Programm.")
sys.exit(1)
if action == 'e':
sys.exit(0)
elif action == 'r':
remove_menu(dry_run=dry_run)
return
try:
edited = get_user_input(t("edited_prompt"), ['y', 'n'])
except RuntimeError:
logging.error("Maximale Anzahl Eingabeversuche überschritten. Beende Programm.")
sys.exit(1)
if edited == 'n':
print(Fore.YELLOW + t("edit_abort") + Style.RESET_ALL)
sys.exit(0)
installed = 0
skipped = 0
for yaml_file in yaml_files:
file_path = CONFIG_DIR / yaml_file
if not test_yaml_file(file_path):
print(Fore.RED + t("skip_invalid_yaml").format(yaml_file) + Style.RESET_ALL)
skipped += 1
continue
try:
install = get_user_input(t("install_confirm").format(yaml_file), ['y', 'n'])
except RuntimeError:
logging.error("Maximale Anzahl Eingabeversuche überschritten. Überspringe Service.")
skipped += 1
continue
if install == 'y':
install_service(yaml_file, dry_run=dry_run)
installed += 1
else:
logging.info(t("install_skipped").format(yaml_file))
skipped += 1
print()
logging.info(t("install_done").format(installed, skipped))
if __name__ == "__main__":
lang_parser, remaining_argv = init_language()
parser = argparse.ArgumentParser(
description=t("script_title"),
parents=[lang_parser]
)
parser.add_argument('--dry-run', action='store_true', help=t("help_dry_run"))
parser.add_argument('--verbose', action='store_true', help=t("help_verbose"))
parser.add_argument('--quiet', action='store_true', help=t("help_quiet"))
args = parser.parse_args(remaining_argv)
setup_logging(verbose=args.verbose, quiet=args.quiet)
if os.geteuid() != 0:
print(Fore.RED + t("not_root") + Style.RESET_ALL)
sys.exit(1)
try:
main(dry_run=args.dry_run)
except KeyboardInterrupt:
print("\nAbbruch durch Benutzer.")
sys.exit(1)
except Exception as e:
logging.critical(f"Unbehandelter Fehler: {e}")
sys.exit(1)
# === Ende des Skripts ===

View file

@ -10,11 +10,14 @@ r"""!
by Bastian Schroll
@file: descriptor.py
@date: 27.10.2019
@date: 04.08.2025
@author: Bastian Schroll
@description: Module to add descriptions to bwPackets
@description: Module to add descriptions to bwPackets with CSV and Regex support
"""
import logging
import csv
import re
import os
from module.moduleBase import ModuleBase
# ###################### #
@ -26,31 +29,172 @@ logging.debug("- %s loaded", __name__)
class BoswatchModule(ModuleBase):
r"""!Adds descriptions to bwPackets"""
r"""!Adds descriptions to bwPackets with CSV and Regex support"""
def __init__(self, config):
r"""!Do not change anything here!"""
super().__init__(__name__, config) # you can access the config class on 'self.config'
def onLoad(self):
r"""!Called by import of the plugin"""
for descriptor in self.config:
if descriptor.get("wildcard", default=None):
self.registerWildcard(descriptor.get("wildcard"), descriptor.get("descrField"))
# Initialize unified cache for all descriptors
self.unified_cache = {}
# Process each descriptor configuration
for descriptor_config in self.config:
scan_field = descriptor_config.get("scanField")
descr_field = descriptor_config.get("descrField")
descriptor_key = f"{scan_field}_{descr_field}"
# Register wildcard if specified
if descriptor_config.get("wildcard", default=None):
self.registerWildcard(descriptor_config.get("wildcard"), descr_field)
# Initialize cache for this descriptor
self.unified_cache[descriptor_key] = []
# Load YAML descriptions first (for backward compatibility)
yaml_descriptions = descriptor_config.get("descriptions", default=None)
if yaml_descriptions:
# yaml_descriptions is a Config object, we need to iterate properly
for desc in yaml_descriptions:
entry = {
'for': str(desc.get("for", default="")),
'add': desc.get("add", default=""),
'isRegex': desc.get("isRegex", default=False) # Default: False
}
# Handle string 'true'/'false' values
if isinstance(entry['isRegex'], str):
entry['isRegex'] = entry['isRegex'].lower() == 'true'
self.unified_cache[descriptor_key].append(entry)
logging.debug("Added YAML entry: %s -> %s", entry['for'], entry['add'])
logging.info("Loaded %d YAML descriptions for %s", len(yaml_descriptions), descriptor_key)
# Load CSV descriptions if csvPath is specified
csv_path = descriptor_config.get("csvPath", default=None)
if csv_path:
self._load_csv_data(csv_path, descriptor_key)
logging.info("Total entries for %s: %d", descriptor_key, len(self.unified_cache[descriptor_key]))
def _load_csv_data(self, csv_path, descriptor_key):
r"""!Load CSV data for a descriptor and add to unified cache"""
try:
if not os.path.isfile(csv_path):
logging.error("CSV file not found: %s", csv_path)
return
csv_count = 0
with open(csv_path, 'r', encoding='utf-8') as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
# Set default values if columns are missing
entry = {
'for': str(row.get('for', '')),
'add': row.get('add', ''),
'isRegex': row.get('isRegex', 'false').lower() == 'true' # Default: False
}
self.unified_cache[descriptor_key].append(entry)
csv_count += 1
logging.info("Loaded %d entries from CSV: %s for %s", csv_count, csv_path, descriptor_key)
except Exception as e:
logging.error("Error loading CSV file %s: %s", csv_path, str(e))
def _find_description(self, descriptor_key, scan_value, bw_packet):
r"""!Find matching description for a scan value with Regex group support."""
descriptions = self.unified_cache.get(descriptor_key, [])
scan_value_str = str(scan_value)
# Search for matching description
for desc in descriptions:
description_text = desc.get('add', '')
match_pattern = desc.get('for', '')
is_regex = desc.get('isRegex', False)
if is_regex:
# Regex matching
try:
match = re.search(match_pattern, scan_value_str)
if match:
# Expand regex groups (\1, \2) in the description
expanded_description = match.expand(description_text)
# Replace standard wildcards like {TONE}
final_description = self._replace_wildcards(expanded_description, bw_packet)
logging.debug("Regex match '%s' -> '%s' for descriptor '%s'",
match_pattern, final_description, descriptor_key)
return final_description
except re.error as e:
logging.error("Invalid regex pattern '%s': %s", match_pattern, str(e))
continue
else:
# Exact match
if match_pattern == scan_value_str:
# Replace standard wildcards like {TONE}
final_description = self._replace_wildcards(description_text, bw_packet)
logging.debug("Exact match '%s' -> '%s' for descriptor '%s'",
match_pattern, final_description, descriptor_key)
return final_description
return None
def _replace_wildcards(self, text, bw_packet):
r"""!Replace all available wildcards in description text dynamically."""
if not text or '{' not in text:
return text
result = text
# Search for wildcards in the format {KEY} and replace them with values from the bw_packet
found_wildcards = re.findall(r"\{([A-Z0-9_]+)\}", result)
for key in found_wildcards:
key_lower = key.lower()
value = bw_packet.get(key_lower)
if value is not None:
result = result.replace(f"{{{key}}}", str(value))
logging.debug("Replaced wildcard {%s} with value '%s'", key, value)
return result
def doWork(self, bwPacket):
r"""!start an run of the module.
@param bwPacket: A BOSWatch packet instance"""
for descriptor in self.config:
if not bwPacket.get(descriptor.get("scanField")):
break # scanField is not available in this packet
bwPacket.set(descriptor.get("descrField"), bwPacket.get(descriptor.get("scanField")))
for description in descriptor.get("descriptions"):
if str(description.get("for")) == bwPacket.get(descriptor.get("scanField")):
logging.debug("Description '%s' added in packet field '%s'",
description.get("add"), descriptor.get("descrField"))
bwPacket.set(descriptor.get("descrField"), description.get("add"))
break # this descriptor has found a description - run next descriptor
logging.debug("Processing packet with mode: %s", bwPacket.get("mode"))
# Process each descriptor configuration
for descriptor_config in self.config:
scan_field = descriptor_config.get("scanField")
descr_field = descriptor_config.get("descrField")
descriptor_key = f"{scan_field}_{descr_field}"
logging.debug("Processing descriptor: scanField='%s', descrField='%s'", scan_field, descr_field)
# Check if scanField is present in packet
scan_value = bwPacket.get(scan_field)
if scan_value is None:
logging.debug("scanField '%s' not found in packet, skipping", scan_field)
continue # scanField not available in this packet - try next descriptor
# Set default value (content of scanField)
bwPacket.set(descr_field, str(scan_value))
logging.debug("Set default value '%s' for field '%s'", scan_value, descr_field)
# Search for matching description in unified cache
description = self._find_description(descriptor_key, scan_value, bwPacket)
if description:
bwPacket.set(descr_field, description)
logging.info("Description set: '%s' -> '%s'", scan_value, description)
else:
logging.debug("No description found for value '%s' in field '%s'", scan_value, scan_field)
logging.debug("Returning modified packet")
return bwPacket
def onUnload(self):

View file

@ -10,126 +10,202 @@ r"""!
by Bastian Schroll
@file: telegram.py
@date: 20.02.2020
@author: Jan Speller
@description: Telegram Plugin
@date: 12.07.2025
@author: Claus Schichl nach der Idee von Jan Speller
@description: Telegram-Plugin mit Retry-Logik ohne externe Telegram-Abhängigkeiten
"""
import logging
import time
import threading
import queue
import requests
from plugin.pluginBase import PluginBase
# ###################### #
# Custom plugin includes #
from telegram.error import (TelegramError, Unauthorized, BadRequest, TimedOut, NetworkError)
from telegram.ext import messagequeue as mq
from telegram.utils.request import Request
import telegram.bot
# ###################### #
logging.debug("- %s loaded", __name__)
# Setup Logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO
)
logger = logging.getLogger(__name__)
class MQBot(telegram.bot.Bot):
'''A subclass of Bot which delegates send method handling to MQ'''
# ===========================
# TelegramSender-Klasse
# ===========================
def __init__(self, *args, is_queued_def=True, mqueue=None, **kwargs):
super(MQBot, self).__init__(*args, **kwargs)
# below 2 attributes should be provided for decorator usage
self._is_messages_queued_default = is_queued_def
self._msg_queue = mqueue or mq.MessageQueue()
class TelegramSender:
def __init__(self, bot_token, chat_ids, max_retries=None, initial_delay=None, max_delay=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.msg_queue = queue.Queue()
self._worker = threading.Thread(target=self._worker_loop, daemon=True)
self._worker.start()
def __del__(self):
def send_message(self, text):
for chat_id in self.chat_ids:
self.msg_queue.put(("text", chat_id, text, 0)) # retry_count = 0
def send_location(self, latitude, longitude):
for chat_id in self.chat_ids:
self.msg_queue.put(("location", chat_id, {"latitude": latitude, "longitude": longitude}, 0))
def _worker_loop(self):
delay = self.initial_delay
while True:
try:
self._msg_queue.stop()
except:
pass
msg_type, chat_id, content, retry_count = self.msg_queue.get()
@mq.queuedmessage
def send_message(self, *args, **kwargs):
'''Wrapped method would accept new `queued` and `isgroup`
OPTIONAL arguments'''
return super(MQBot, self).send_message(*args, **kwargs)
success, permanent_failure, custom_delay = self._send_to_telegram(msg_type, chat_id, content)
if success:
delay = self.initial_delay
elif permanent_failure:
logger.error("Permanenter Fehler Nachricht wird verworfen.")
elif retry_count >= self.max_retries:
logger.error("Maximale Wiederholungsanzahl erreicht Nachricht wird verworfen.")
else:
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
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)
delay = min(delay * 2, self.max_delay)
except Exception as e:
logger.exception(f"Fehler im Telegram-Worker: {e}")
time.sleep(5)
def _send_to_telegram(self, msg_type, chat_id, content):
if msg_type == "text":
url = f"https://api.telegram.org/bot{self.bot_token}/sendMessage"
payload = {
'chat_id': chat_id,
'text': content
}
elif msg_type == "location":
url = f"https://api.telegram.org/bot{self.bot_token}/sendLocation"
payload = {
'chat_id': chat_id,
**content
}
else:
logger.error("Unbekannter Nachrichtentyp.")
return False, True, None # Unbekannter Typ = permanent falsch
try:
custom_delay = None # Standardwert für Rückgabe, außer bei 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
if response.status_code == 400:
logger.error("Ungültige Parameter Nachricht wird nicht erneut gesendet.")
return False, True, custom_delay # Permanent fehlerhaft
if response.status_code == 401:
logger.critical("Ungültiger Bot-Token bitte prüfen!")
return False, True, custom_delay # Permanent fehlerhaft
response.raise_for_status()
logger.info(f"Erfolgreich gesendet an Chat-ID {chat_id}")
return True, False, custom_delay
except requests.RequestException as e:
logger.warning(f"Fehler beim Senden an Telegram (Chat-ID {chat_id}): {e}")
return False, False, custom_delay
# ===========================
# BoswatchPlugin-Klasse
# ===========================
class BoswatchPlugin(PluginBase):
r"""!Description of the Plugin"""
def __init__(self, config):
r"""!Do not change anything here!"""
super().__init__(__name__, config) # you can access the config class on 'self.config'
def onLoad(self):
r"""!Called by import of the plugin"""
if self.config.get("queue", default=True):
q = mq.MessageQueue()
request = Request(con_pool_size=8)
self.bot = MQBot(token=self.config.get("botToken", default=""), request=request, mqueue=q)
print('queue')
else:
self.bot = telegram.Bot(token=self.config.get("botToken"))
print('normal')
bot_token = self.config.get("botToken")
chat_ids = self.config.get("chatIds", default=[])
if not bot_token or not chat_ids:
logger.error("botToken oder chatIds fehlen in der Konfiguration!")
return
# Konfigurierbare Parameter mit Fallback-Defaults
max_retries = self.config.get("max_retries")
initial_delay = self.config.get("initial_delay")
max_delay = self.config.get("max_delay")
self.sender = TelegramSender(
bot_token=bot_token,
chat_ids=chat_ids,
max_retries=max_retries,
initial_delay=initial_delay,
max_delay=max_delay
)
startup_message = self.config.get("startup_message")
if startup_message and startup_message.strip():
self.sender.send_message(startup_message)
def setup(self):
r"""!Called before alarm
Remove if not implemented"""
pass
def fms(self, bwPacket):
r"""!Called on FMS alarm
@param bwPacket: bwPacket instance"""
msg = self.parseWildcards(self.config.get("message_fms", default="{FMS}"))
self._sendMessage(msg)
self.sender.send_message(msg)
def pocsag(self, bwPacket):
r"""!Called on POCSAG alarm
@param bwPacket: bwPacket instance"""
msg = self.parseWildcards(self.config.get("message_pocsag", default="{RIC}({SRIC})\n{MSG}"))
self._sendMessage(msg)
self.sender.send_message(msg)
if bwPacket.get("lat") is not None and bwPacket.get("lon") is not None:
logging.debug("Found coordinates in packet")
(lat, lon) = (bwPacket.get("lat"), bwPacket.get("lon"))
self._sendLocation(lat, lon)
lat, lon = bwPacket.get("lat"), bwPacket.get("lon")
logger.debug("Koordinaten gefunden sende Standort.")
self.sender.send_location(lat, lon)
def zvei(self, bwPacket):
r"""!Called on ZVEI alarm
@param bwPacket: bwPacket instance"""
msg = self.parseWildcards(self.config.get("message_zvei", default="{TONE}"))
self._sendMessage(msg)
self.sender.send_message(msg)
def msg(self, bwPacket):
r"""!Called on MSG packet
@param bwPacket: bwPacket instance"""
msg = self.parseWildcards(self.config.get("message_msg"))
self._sendMessage(msg)
self.sender.send_message(msg)
def _sendMessage(self, message):
for chatId in self.config.get("chatIds", default=[]):
try:
# Send Message via Telegram
logging.info("Sending message to " + chatId)
self.bot.send_message(chat_id=chatId, text=message)
def teardown(self):
r"""!Called after alarm
Remove if not implemented"""
pass
except Unauthorized:
logging.exception("Error while sending Telegram Message, please Check your api-key")
except (TimedOut, NetworkError):
logging.exception("Error while sending Telegram Message, please Check your connectivity")
except (BadRequest, TelegramError):
logging.exception("Error while sending Telegram Message")
except Exception as e:
logging.exception("Unknown Error while sending Telegram Message: " + str(type(e).__name__) + ": " + str(e))
def _sendLocation(self, lat, lon):
for chatId in self.config.get("chatIds", default=[]):
try:
# Send Location via Telegram
if lat is not None and lon is not None:
logging.info("Sending location to " + chatId)
self.bot.sendLocation(chat_id=chatId, latitude=lat, longitude=lon)
except Unauthorized:
logging.exception("Error while sending Telegram Message, please Check your api-key")
except (TimedOut, NetworkError):
logging.exception("Error while sending Telegram Message, please Check your connectivity")
except (BadRequest, TelegramError):
logging.exception("Error while sending Telegram Message")
except Exception as e:
logging.exception("Unknown Error while sending Telegram Message: " + str(type(e).__name__) + ": " + str(e))
def onUnload(self):
r"""!Called by destruction of the plugin
Remove if not implemented"""
pass