├── test ├── __init__.py ├── module │ ├── __init__.py │ └── test_descriptor.py ├── plugin │ └── __init__.py ├── boswatch │ ├── __init__.py │ ├── test_header.py │ ├── test_paths.py │ ├── test_packet.py │ ├── test_broadcast.py │ ├── test_timer.py │ ├── test_config.py │ └── test_decoder.py ├── test_configFailed.yaml ├── test_config.yaml ├── pytest.ini ├── test_template.py └── testdata.list ├── module ├── __init__.py ├── filter │ ├── __init__.py │ ├── modeFilter.py │ ├── regexFilter.py │ └── doubleFilter.py ├── template_module.py ├── geocoding.py └── moduleBase.py ├── plugin ├── __init__.py ├── template_plugin.py ├── http.py ├── divera.py └── pluginBase.py ├── boswatch ├── __init__.py ├── decoder │ ├── __init__.py │ ├── decoder.py │ ├── zveiDecoder.py │ ├── fmsDecoder.py │ └── pocsagDecoder.py ├── network │ ├── __init__.py │ ├── netCheck.py │ ├── client.py │ └── broadcast.py ├── router │ ├── __init__.py │ ├── route.py │ └── router.py ├── utils │ ├── __init__.py │ ├── version.py │ ├── paths.py │ ├── misc.py │ └── header.py ├── inputSource │ ├── __init__.py │ ├── lineInInput.py │ ├── pulseaudioInput.py │ ├── sdrInput.py │ └── inputBase.py ├── packet.py ├── configYaml.py ├── timer.py ├── wildcard.py └── processManager.py ├── logo ├── bw3.png └── dev.png ├── pytest.sh ├── docu ├── docs │ ├── img │ │ ├── bw3.png │ │ ├── client.png │ │ ├── router.png │ │ ├── server.png │ │ ├── broadcast.png │ │ ├── client.drawio │ │ ├── broadcast.drawio │ │ ├── server.drawio │ │ └── router.drawio │ ├── tbd.md │ ├── usage.md │ ├── index.md │ ├── changelog.md │ ├── plugin │ │ ├── mysql.md │ │ ├── http.md │ │ ├── divera.md │ │ └── telegram.md │ ├── stylesheets │ │ └── extra.css │ ├── modul │ │ ├── mode_filter.md │ │ ├── double_filter.md │ │ ├── geocoding.md │ │ ├── regex_filter.md │ │ └── descriptor.md │ ├── information │ │ ├── serverclient.md │ │ ├── broadcast.md │ │ └── router.md │ ├── develop │ │ ├── packet.md │ │ └── ModulPlugin.md │ ├── install.md │ └── service.md └── mkdocs.yml ├── requirements.txt ├── .github ├── FUNDING.yml └── workflows │ ├── run_pytest.yml │ ├── build_docs.yml │ └── codeql-analysis.yml ├── .gitignore ├── FileHead.template.py ├── config ├── server.yaml ├── client.yaml ├── logger_client.ini └── logger_server.ini ├── README.md ├── Dockerfile ├── init_db.sql └── bw_server.py /test/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | -------------------------------------------------------------------------------- /test/module/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | -------------------------------------------------------------------------------- /test/plugin/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | -------------------------------------------------------------------------------- /test/boswatch/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | -------------------------------------------------------------------------------- /module/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /plugin/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /boswatch/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /boswatch/decoder/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /boswatch/network/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /boswatch/router/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /boswatch/utils/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /logo/bw3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOSWatch/BW3-Core/HEAD/logo/bw3.png -------------------------------------------------------------------------------- /logo/dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOSWatch/BW3-Core/HEAD/logo/dev.png -------------------------------------------------------------------------------- /module/filter/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /boswatch/inputSource/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /pytest.sh: -------------------------------------------------------------------------------- 1 | source ./venv/bin/activate 2 | pytest -c test/pytest.ini 3 | deactivate -------------------------------------------------------------------------------- /docu/docs/img/bw3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOSWatch/BW3-Core/HEAD/docu/docs/img/bw3.png -------------------------------------------------------------------------------- /docu/docs/img/client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOSWatch/BW3-Core/HEAD/docu/docs/img/client.png -------------------------------------------------------------------------------- /docu/docs/img/router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOSWatch/BW3-Core/HEAD/docu/docs/img/router.png -------------------------------------------------------------------------------- /docu/docs/img/server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOSWatch/BW3-Core/HEAD/docu/docs/img/server.png -------------------------------------------------------------------------------- /docu/docs/img/broadcast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOSWatch/BW3-Core/HEAD/docu/docs/img/broadcast.png -------------------------------------------------------------------------------- /docu/docs/tbd.md: -------------------------------------------------------------------------------- 1 | #
To be done ...
2 | --- 3 | 4 | Hier existiert noch kein Inhalt, gerne kannst du uns aber Helfen die Dokumentation zu vervollständigen. 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # for pip to install all needed 2 | # called with 'pip install -r requirements.txt' 3 | pyyaml 4 | 5 | # for documentation generating 6 | mkdocs 7 | 8 | # for develope only 9 | pytest 10 | pytest-cov 11 | flake8==6.1.0 12 | pytest-flake8 13 | pytest-flakes 14 | pytest-randomly 15 | -------------------------------------------------------------------------------- /test/test_configFailed.yaml: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ____ ____ ______ __ __ __ _____ 3 | # / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 4 | # / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 5 | # / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 6 | #/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 7 | # German BOS Information Script 8 | # 9 | 10 | # for the test_config - a config failing while loading 11 | types: 12 | failedIntend: Hello World # this line has false indentation 13 | rightIntend: Hello World 14 | 15 | -------------------------------------------------------------------------------- /docu/docs/usage.md: -------------------------------------------------------------------------------- 1 | # 🇩🇪 Start von BOSWatch3 2 | Nach dem Neustart kannst du BOSWatch3 wie folgt starten: 3 | 4 | ```bash 5 | cd /opt/boswatch3 6 | sudo python3 bw_client.py -c config/client.yaml 7 | sudo python3 bw_server.py -c config/server.yaml 8 | ``` 9 | 10 | ## Optional: Als Dienst einrichten 11 | Weiter gehts mit [als Service einrichten](service.md) 12 | 13 | --- 14 | 15 | # 🇬🇧 Starting BOSWatch3 16 | After reboot, you can start BOSWatch3 as follows: 17 | 18 | ```bash 19 | cd /opt/boswatch3 20 | sudo python3 bw_client.py -c config/client.yaml 21 | sudo python3 bw_server.py -c config/server.yaml 22 | ``` 23 | 24 | ## Optional: Setup as a Service 25 | For further instructions, see [Setup as a Service](service.md) -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Schrolli91] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /docu/docs/index.md: -------------------------------------------------------------------------------- 1 | #
BOSWatch 3
2 | --- 3 | 4 | ![BOSWatch](img/bw3.png){ .center } 5 | 6 | 7 | **Es wird darauf hingewiesen, dass für die Teilnahme am BOS-Funk nur nach den Technischen Richtlinien der BOS zugelassene Funkanlagen verwendet werden dürfen.** 8 | **Der BOS-Funk ist ein nichtöffentlicher mobiler Landfunk. Privatpersonen gehören nicht zum Kreis der berechtigten Funkteilnehmer.** _(Quelle: TR-BOS)_ 9 | 10 | --- 11 | 12 | **The intercept of the German BOS radio is strictly prohibited and will be prosecuted. The use is only permitted for authorized personnel.** 13 | 14 | --- 15 | 16 | Falls du uns unterstützen möchtest würden wir uns über eine Spende freuen. 17 | Server, Hosting, Domain sowie Kaffee kosten leider Geld ;-) 18 | 19 | [![DonateMe](https://www.paypalobjects.com/de_DE/DE/i/btn/btn_donate_LG.gif)](https://www.paypal.me/BSchroll){ .center } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ____ ____ ______ __ __ __ _____ 2 | # / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 3 | # / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 4 | # / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 5 | #/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 6 | # German BOS Information Script 7 | # by Bastian Schroll 8 | 9 | # virtual environment 10 | \venv/ 11 | 12 | # generated files 13 | stats_* 14 | log/ 15 | docu/docs/api/html 16 | docu/site/ 17 | 18 | # Logo Photoshop file 19 | *.psd 20 | 21 | # Coverage Report 22 | \.coverage 23 | 24 | # Python precompiled 25 | *.pyc 26 | 27 | # Visual Studio Code 28 | \.vscode/ 29 | \.pytest_cache/ 30 | 31 | # PyCharm IDE 32 | \.cache/ 33 | \.idea/ 34 | 35 | # Eclipse IDE (PyDev) 36 | \.settings/ 37 | \.project 38 | \.pydevproject 39 | -------------------------------------------------------------------------------- /boswatch/utils/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: version.py 13 | @date: 14.12.2017 14 | @author: Bastian Schroll 15 | @description: Version numbers, branch and release date of BOSWatch 16 | """ 17 | import logging 18 | 19 | logging.debug("- %s loaded", __name__) 20 | 21 | client = {"major": 3, "minor": 0, "patch": 0} 22 | server = {"major": 3, "minor": 0, "patch": 0} 23 | date = {"day": 1, "month": 1, "year": 2019} 24 | branch = "develop" 25 | -------------------------------------------------------------------------------- /docu/docs/changelog.md: -------------------------------------------------------------------------------- 1 | #
Changelog
2 | --- 3 | 4 | ## Version [2.9.0] - date 5 | 6 | Functions implemented in initial version: 7 | 8 | - Multithreaded Server/Client infrastructure for alarm handling 9 | - Client can auto fetch connection information over bradocast from server 10 | - Easy configuration with YAML file for all components 11 | - Simple module and plugin system to extend functionality 12 | - Alarmpacket routing system for flexible chains of modules nd plugins 13 | 14 | ### Modules 15 | - Mode filter to filter at specific packet types such as FMS, POCSAG, ZVEI or MSG packets 16 | 17 | ### Filter 18 | 19 | 20 | --- 21 | 22 | Zum schreiben des Changelog's siehe: [http://keepachangelog.com/de/1.0.0/](http://keepachangelog.com/de/1.0.0/) 23 | 24 | 33 | -------------------------------------------------------------------------------- /FileHead.template.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: FileHead.template.py 13 | @date: ##.##.2018 14 | @author: Bastian Schroll 15 | @description: That is the FileHead that should be used for all files 16 | """ 17 | import logging 18 | 19 | # from boswatch.module import file 20 | 21 | logging.debug("- %s loaded", __name__) 22 | 23 | 24 | class ClassName: 25 | r"""!General class comment""" 26 | 27 | def __init__(self): 28 | r"""!init comment""" 29 | pass 30 | -------------------------------------------------------------------------------- /docu/docs/plugin/mysql.md: -------------------------------------------------------------------------------- 1 | #
Mysql
2 | --- 3 | 4 | ## Beschreibung 5 | Mit diesem Plugin ist es moeglich, die Alarmierungen in einer Mysql / Mariadb Datenbank zu speichern. 6 | Das Plugin legt die Tabelle "boswatch" selbststaendig an, wenn diese nicht vorhanden ist. 7 | 8 | ## Unterstütze Alarmtypen 9 | - Fms 10 | - Pocsag 11 | - Zvei 12 | - Msg 13 | 14 | ## Resource 15 | `mysql` 16 | 17 | ## Konfiguration 18 | |Feld|Beschreibung|Default| 19 | |----|------------|-------| 20 | |host|IP-Adresse bzw. URL des Hosts|| 21 | |user|Username|| 22 | |password|Passwort|| 23 | |database|Name der Datenbank|| 24 | 25 | **Beispiel:** 26 | ```yaml 27 | - type: plugin 28 | name: mysql 29 | res: mysql 30 | config: 31 | host: HOST 32 | user: USERNAME 33 | password: PASSWORD 34 | database: DATABASE 35 | ``` 36 | 37 | --- 38 | ## Modul Abhängigkeiten 39 | - keine 40 | 41 | --- 42 | ## Externe Abhängigkeiten 43 | - mysql-connector-python 44 | -------------------------------------------------------------------------------- /docu/docs/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | /* Zentriert Bilder mit dem Attribut align="center" */ 2 | img[align="center"] { 3 | display: block; 4 | margin-left: auto; 5 | margin-right: auto; 6 | } 7 | 8 | /* Optional: zentriere auch Bilder mit einer CSS-Klasse .center */ 9 | img.center { 10 | display: block; 11 | margin-left: auto; 12 | margin-right: auto; 13 | } 14 | 15 | /* Optional: zentrierter Text via .center-text */ 16 | .center-text { 17 | text-align: center; 18 | } 19 | /* Optional: zentrierter Text via .text-center */ 20 | .text-center { 21 | text-align: center; 22 | } 23 | 24 | /* Zentriert Bilder in einem Link -Tag */ 25 | /* Diese Regel gilt für Bilder, die in einem Link-Tag eingebettet sind */ 26 | a img[align="center"] { 27 | display: block; 28 | margin-left: auto; 29 | margin-right: auto; 30 | } 31 | 32 | /* Optional: zentriert Bilder in einem Link-Tag mit der Klasse .center */ 33 | a.center img { 34 | display: block; 35 | margin: 0 auto; 36 | } -------------------------------------------------------------------------------- /docu/docs/modul/mode_filter.md: -------------------------------------------------------------------------------- 1 | #
Mode Filter
2 | --- 3 | 4 | ## Beschreibung 5 | Mit diesem Modul ist es möglich, die Pakete auf bestimmte Modes (FMS, POCSAG, ZVEI) zu Filtern. Je nach Konfiguration werden Pakete eines bestimmten Modes im aktuellen Router weitergeleitet oder verworfen. 6 | 7 | ## Unterstütze Alarmtypen 8 | - Fms 9 | - Pocsag 10 | - Zvei 11 | - Msg 12 | 13 | ## Resource 14 | `filter.modeFilter` 15 | 16 | ## Konfiguration 17 | |Feld|Beschreibung|Default| 18 | |----|------------|-------| 19 | |allowed|Liste der erlaubten Paket Typen `fms` `zvei` `pocsag` `msg`|| 20 | 21 | **Beispiel:** 22 | ```yaml 23 | - type: module 24 | res: filter.modeFilter 25 | config: 26 | allowed: 27 | - fms 28 | - pocsag 29 | ``` 30 | 31 | --- 32 | ## Modul Abhängigkeiten 33 | - keine 34 | 35 | --- 36 | ## Externe Abhängigkeiten 37 | - keine 38 | 39 | --- 40 | ## Paket Modifikationen 41 | - keine 42 | 43 | --- 44 | ## Zusätzliche Wildcards 45 | - keine 46 | 47 | -------------------------------------------------------------------------------- /config/server.yaml: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ____ ____ ______ __ __ __ _____ 3 | # / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 4 | # / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 5 | # / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 6 | #/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 7 | # German BOS Information Script 8 | # by Bastian Schroll 9 | 10 | server: 11 | port: 8080 12 | name: BW3 Server # name of the BW3 Server instance 13 | useBroadcast: no # serve server ip on broadcast request 14 | logging: False # enable log file 15 | 16 | alarmRouter: 17 | - Router 1 18 | 19 | router: 20 | - name: Router 1 21 | route: 22 | - type: module 23 | res: filter.modeFilter 24 | name: Filter Fms/Zvei 25 | config: 26 | allowed: 27 | - fms 28 | - zvei 29 | - type: plugin 30 | name: test plugin 31 | res: template_plugin 32 | -------------------------------------------------------------------------------- /test/test_config.yaml: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ____ ____ ______ __ __ __ _____ 3 | # / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 4 | # / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 5 | # / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 6 | #/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 7 | # German BOS Information Script 8 | # 9 | 10 | # for the test_config 11 | types: 12 | string: Hello World 13 | bool: true 14 | integer: 11 15 | float: 3.14 16 | 17 | # for the test_config 18 | list: 19 | - one 20 | - two 21 | - three 22 | 23 | list1: 24 | - list11: 25 | - one 26 | - two 27 | - three 28 | - list12: 29 | - one 30 | - two 31 | - three 32 | - string1 33 | 34 | descriptor_test: 35 | - scanField: tone 36 | descrField: description 37 | descriptions: 38 | - for: 12345 39 | add: Test 12345 40 | - for: 23456 41 | add: Test 23456 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BOSWatch 2 | 3 | ![BOSWatch](logo/bw3.png "BOSWatch 3 Logo") 4 | 5 | ![pytest](https://github.com/BOSWatch/BW3-Core/workflows/pytest/badge.svg) 6 | ![documentation](https://github.com/BOSWatch/BW3-Core/workflows/build_docs/badge.svg) 7 | ![CodeQL](https://github.com/BOSWatch/BW3-Core/workflows/CodeQL/badge.svg) 8 | 9 | **Es wird darauf hingewiesen, dass für die Teilnahme am BOS-Funk nur nach den Technischen Richtlinien der BOS zugelassene Funkanlagen verwendet werden dürfen.** 10 | **Der BOS-Funk ist ein nichtöffentlicher mobiler Landfunk. Privatpersonen gehören nicht zum Kreis der berechtigten Funkteilnehmer.** _(Quelle: TR-BOS)_ 11 | 12 | ### Documentation: [https://docs.boswatch.de](https://docs.boswatch.de) 13 | 14 | Wenn dir dieses Projekt gefällt, gib uns bitte einen **STAR** 15 | 16 | *** 17 | 18 | **The intercept of the German BOS radio is strictly prohibited and will be prosecuted. The use is only permitted for authorized personnel.** 19 | 20 | If you like this project, please give us a **STAR** 21 | -------------------------------------------------------------------------------- /.github/workflows/run_pytest.yml: -------------------------------------------------------------------------------- 1 | name: pytest 2 | permissions: 3 | contents: read 4 | 5 | on: 6 | push: 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest] 15 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 16 | runs-on: ${{matrix.os}} 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - name: Set up Python ${{matrix.python-version}} at ${{matrix.os}} 22 | uses: actions/setup-python@v3 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install -r requirements.txt 30 | mkdir -p log/ 31 | 32 | - name: Test with pytest 33 | run: | 34 | pytest -c 'test/pytest.ini' 35 | 36 | - name: Save artifacts 37 | uses: actions/upload-artifact@v4 38 | with: 39 | name: test-${{ matrix.python-version }}.log 40 | path: log/test.log 41 | -------------------------------------------------------------------------------- /docu/docs/plugin/http.md: -------------------------------------------------------------------------------- 1 | #
Http
2 | --- 3 | 4 | ## Beschreibung 5 | Mit diesem Plugin ist es moeglich, Http-Anfragen für Alarmierungen zu senden. 6 | Wildcards in den Urls werden automatisch ersetzt. 7 | 8 | ## Unterstütze Alarmtypen 9 | - Fms 10 | - Pocsag 11 | - Zvei 12 | - Msg 13 | 14 | ## Resource 15 | `http` 16 | 17 | ## Konfiguration 18 | |Feld|Beschreibung|Default| 19 | |----|------------|-------| 20 | |fms|Liste mit Urls für Fms-Alarmierung|| 21 | |pocsag|Liste mit Urls für Pocsag-Alarmierung|| 22 | |zvei|Liste mit Urls für Zvei-Alarmierung|| 23 | |msg|Liste mit Urls für Msg-Alarmierung|| 24 | 25 | **Beispiel:** 26 | ```yaml 27 | - type: plugin 28 | name: HTTP Plugin 29 | res: http 30 | config: 31 | pocsag: 32 | - "http://google.com?q={MSG}" 33 | - "http://duckduckgo.com?q={MSG}" 34 | fms: 35 | - "http://duckduckgo.com?q={LOC}" 36 | ``` 37 | 38 | --- 39 | ## Modul Abhängigkeiten 40 | - keine 41 | 42 | --- 43 | ## Externe Abhängigkeiten 44 | - asyncio 45 | - aiohttp 46 | -------------------------------------------------------------------------------- /test/pytest.ini: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ____ ____ ______ __ __ __ _____ 3 | # / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 4 | # / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 5 | # / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 6 | #/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 7 | # German BOS Information Script 8 | # by Bastian Schroll 9 | 10 | [pytest] 11 | addopts = -v --flake8 --flakes --cov=boswatch/ --cov=module/ --cov plugin/ --cov-report=term-missing --log-level=CRITICAL 12 | 13 | # classic or progress 14 | console_output_style = progress 15 | 16 | log_file=log/test.log 17 | log_file_level=debug 18 | log_file_format=%(asctime)s - %(module)-12s %(funcName)-15s [%(levelname)-8s] %(message)s 19 | log_file_date_format=%d.%m.%Y %H:%M:%S 20 | 21 | #flake8 plugin 22 | flake8-ignore = E402 E501 E722 W504 W605 23 | # E402 # import not at top 24 | # E501 # line too long 25 | # E722 # do not use bare 'except' 26 | # W504 # line break after binary operator 27 | # W605 # invalid escape sequence 28 | # flake8-max-line-length = 99 -------------------------------------------------------------------------------- /docu/docs/modul/double_filter.md: -------------------------------------------------------------------------------- 1 | #
Double Filter
2 | --- 3 | 4 | ## Beschreibung 5 | Mit diesem Modul ist es möglich, die Pakete auf Duplikate zu Filtern. Je nach Konfiguration werden doppelte Pakete im aktuellen Router weitergeleitet oder verworfen. 6 | 7 | ## Unterstütze Alarmtypen 8 | - Fms 9 | - Pocsag 10 | - Zvei 11 | 12 | ## Resource 13 | `filter.doubleFilter` 14 | 15 | ## Konfiguration 16 | |Feld|Beschreibung|Default| 17 | |----|------------|-------| 18 | |ignoreTime|Zeitfenster für doppelte Pakte in Sekunden|10| 19 | |maxEntry|Maximale Anzahl an Paketen in der Vergleichsliste|20| 20 | |pocsagFields|Liste der Pocsag Felder zum Vergleichen: `ric`, `subric` und/oder `message`|`ric,subric`| 21 | 22 | **Beispiel:** 23 | ```yaml 24 | - type: module 25 | res: filter.doubleFilter 26 | config: 27 | ignoreTime: 30 28 | maxEntry: 10 29 | pocsagFields: 30 | - ric 31 | - subric 32 | ``` 33 | 34 | --- 35 | ## Modul Abhängigkeiten 36 | - keine 37 | 38 | --- 39 | ## Externe Abhängigkeiten 40 | - keine 41 | 42 | --- 43 | ## Paket Modifikationen 44 | - keine 45 | 46 | --- 47 | ## Zusätzliche Wildcards 48 | - keine 49 | 50 | -------------------------------------------------------------------------------- /test/boswatch/test_header.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: test_header.py 13 | @date: 12.12.2017 14 | @author: Bastian Schroll 15 | @description: Unittests for BOSWatch. File have to run as "pytest" unittest 16 | """ 17 | # problem of the pytest fixtures 18 | # pylint: disable=redefined-outer-name 19 | import logging 20 | 21 | from boswatch.utils import header 22 | 23 | 24 | def setup_function(function): 25 | logging.debug("[TEST] %s.%s", function.__module__, function.__name__) 26 | 27 | 28 | def test_logoToLog(): 29 | r"""!Test logo to log""" 30 | assert header.logoToLog() 31 | 32 | 33 | def test_infoToLog(): 34 | r"""!Test info to log""" 35 | assert header.infoToLog() 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.13 AS build-base 2 | RUN apk add git make cmake g++ libusb-dev libpulse 3 | 4 | FROM build-base AS rtl_fm 5 | ARG RTL_SDR_VERSION=0.6.0 6 | RUN git clone --depth 1 --branch ${RTL_SDR_VERSION} https://github.com/osmocom/rtl-sdr.git /opt/rtl_sdr 7 | WORKDIR /opt/rtl_sdr/build 8 | RUN cmake .. && make 9 | 10 | FROM build-base AS multimon 11 | ARG MULTIMON_VERSION=1.1.9 12 | RUN git clone --depth 1 --branch ${MULTIMON_VERSION} https://github.com/EliasOenal/multimon-ng.git /opt/multimon 13 | WORKDIR /opt/multimon/build 14 | RUN cmake .. && make 15 | 16 | FROM alpine:3.13 AS boswatch 17 | ARG BW_VERSION=develop 18 | RUN apk add git && \ 19 | git clone --depth 1 --branch ${BW_VERSION} https://github.com/BOSWatch/BW3-Core.git /opt/boswatch 20 | 21 | 22 | FROM python:3.9.1-alpine AS runner 23 | LABEL maintainer="bastian@schroll-software.de" 24 | 25 | # for RTL for MM 26 | RUN apk add libusb-dev libpulse && \ 27 | pip3 install pyyaml 28 | 29 | COPY --from=boswatch /opt/boswatch/ /opt/boswatch/ 30 | COPY --from=multimon /opt/multimon/build/multimon-ng /opt/multimon-ng 31 | COPY --from=rtl_fm /opt/rtl_sdr/build/src/ /opt/rtl_sdr 32 | -------------------------------------------------------------------------------- /test/test_template.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: test_template.py 13 | @date: 03.03.2019 14 | @author: Bastian Schroll 15 | @description: Unittests for BOSWatch. File have to run as "pytest" unittest 16 | """ 17 | # problem of the pytest fixtures 18 | # pylint: disable=redefined-outer-name 19 | import logging 20 | import pytest 21 | 22 | 23 | def setup_method(method): 24 | logging.debug("[TEST] %s.%s", method.__module__, method.__name__) 25 | 26 | 27 | @pytest.fixture 28 | def fixtureTemplate(): 29 | return None 30 | 31 | 32 | @pytest.mark.skip("Reason why i will skipped") 33 | def test_skippedTest(): 34 | pass 35 | 36 | 37 | def test_testName(): 38 | pass 39 | 40 | 41 | def test_withFixture(fixtureTemplate): 42 | assert fixtureTemplate is None 43 | -------------------------------------------------------------------------------- /.github/workflows/build_docs.yml: -------------------------------------------------------------------------------- 1 | name: build_docs 2 | 3 | on: 4 | push: 5 | branches: 6 | #- master 7 | - develop 8 | 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | concurrency: 15 | group: "pages" 16 | cancel-in-progress: false 17 | 18 | jobs: 19 | build_docs: 20 | name: Build documentation 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | - name: Setup Pages 26 | uses: actions/configure-pages@v5 27 | - name: Install dependencies 28 | run: | 29 | sudo apt-get update 30 | sudo apt-get install -y doxygen 31 | pip install mkdocs 32 | - name: Build doxygen 33 | run: doxygen docu/doxygen.ini 34 | - name: Build MkDocs site 35 | working-directory: docu 36 | run: mkdocs build --site-dir site 37 | - name: Upload artifact 38 | uses: actions/upload-pages-artifact@v3 39 | with: 40 | path: ./docu/site 41 | 42 | # Deployment job 43 | deploy: 44 | environment: 45 | name: github-pages 46 | url: ${{ steps.deployment.outputs.page_url }} 47 | runs-on: ubuntu-latest 48 | needs: build_docs 49 | steps: 50 | - name: Deploy to GitHub Pages 51 | id: deployment 52 | uses: actions/deploy-pages@v4 53 | -------------------------------------------------------------------------------- /docu/docs/modul/geocoding.md: -------------------------------------------------------------------------------- 1 | #
Geocoding
2 | --- 3 | 4 | ## Beschreibung 5 | Mit diesem Modul können einem Paket die Koordinaten eines Ortes oder einer Adresse angefügt werden. 6 | 7 | ## Unterstützte Alarmtypen 8 | - Pocsag 9 | 10 | ## Resource 11 | `geocoding` 12 | 13 | ## Konfiguration 14 | |Feld|Beschreibung|Default| 15 | |----|------------|-------| 16 | apiProvider|Der Provider für das Geocoding| 17 | apiToken|Der Api-Token fuer die Geocoding-Api| 18 | geoRegex|Regex Capture-Group zum Herausfiltern der Adresse| 19 | 20 | #### Verfügbare Geocoding Provider 21 | |Name|Einstellungswert| 22 | |----|------------| 23 | |Mapbox|mapbox| 24 | |Google Maps|google| 25 | 26 | **Beispiel:** 27 | ```yaml 28 | - type: module 29 | name: Geocoding Module 30 | res: geocoding 31 | config: 32 | apiProvider: "{{ Provider für Geocoding }}" 33 | apiToken: "{{ API-Key für Provider }}" 34 | regex: "((?:[^ ]*,)*?)" 35 | ``` 36 | 37 | --- 38 | ## Modul Abhängigkeiten 39 | - keine 40 | 41 | --- 42 | ## Externe Abhängigkeiten 43 | - geocoder 44 | 45 | --- 46 | ## Paket Modifikationen 47 | - `address`: gefundene Adresse 48 | - `lat`: Latitude der Adresse 49 | - `lon`: Longitude der Adresse 50 | 51 | --- 52 | ## Zusätzliche Wildcards 53 | - `{ADDRESS}`: gefundene Adresse 54 | - `{LAT}`: Latitude der Adresse 55 | - `{LON}`: Longitude der Adresse -------------------------------------------------------------------------------- /init_db.sql: -------------------------------------------------------------------------------- 1 | create table boswatch 2 | ( 3 | id int auto_increment primary key, 4 | packetTimestamp timestamp default now() not null, 5 | packetMode enum('fms', 'pocsag', 'zvei', 'msg') not null, 6 | pocsag_ric char(7) default null, 7 | pocsag_subric enum('1', '2', '3', '4') default null, 8 | pocsag_subricText enum('a', 'b', 'c', 'd') default null, 9 | pocsag_message text default null, 10 | pocsag_bitrate enum('512', '1200', '2400') default null, 11 | zvei_tone char(5) default null, 12 | fms_fms char(8) default null, 13 | fms_service varchar(255) default null, 14 | fms_country varchar(255) default null, 15 | fms_location varchar(255) default null, 16 | fms_vehicle varchar(255) default null, 17 | fms_status char(1) default null, 18 | fms_direction char(1) default null, 19 | fms_directionText tinytext default null, 20 | fms_tacticalInfo char(3) default null, 21 | serverName varchar(255) not null, 22 | serverVersion varchar(100) not null, 23 | serverBuildDate varchar(255) not null, 24 | serverBranch varchar(255) not null, 25 | clientName varchar(255) not null, 26 | clientIP varchar(255) not null, 27 | clientVersion varchar(100) not null, 28 | clientBuildDate varchar(255) not null, 29 | clientBranch varchar(255) not null, 30 | inputSource varchar(30) not null, 31 | frequency varchar(30) not null 32 | ); 33 | create unique index boswatch_id_uindex 34 | on boswatch (id); 35 | 36 | -------------------------------------------------------------------------------- /boswatch/router/route.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: route.py 13 | @date: 04.03.2019 14 | @author: Bastian Schroll 15 | @description: Class for a single BOSWatch packet router route point 16 | """ 17 | 18 | import logging 19 | 20 | logging.debug("- %s loaded", __name__) 21 | 22 | 23 | class Route: 24 | r"""!Class for single routing points""" 25 | def __init__(self, name, callback, statsCallback=None, cleanupCallback=None): 26 | r"""!Create a instance of an route point 27 | 28 | @param name: name of the route point 29 | @param callback: instance of the callback function 30 | @param statsCallback: instance of the callback to get statistics (None) 31 | @param cleanupCallback: instance of the callback to run a cleanup method (None) 32 | """ 33 | self.name = name 34 | self.callback = callback 35 | self.statistics = statsCallback 36 | self.cleanup = cleanupCallback 37 | -------------------------------------------------------------------------------- /config/client.yaml: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ____ ____ ______ __ __ __ _____ 3 | # / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 4 | # / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 5 | # / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 6 | #/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 7 | # German BOS Information Script 8 | # by Bastian Schroll 9 | 10 | client: 11 | name: BW3 Client # name of the BW3 Client instance 12 | inputSource: sdr # name of the input source('sdr' or 'lineIn') 13 | useBroadcast: no # use broadcast to find server automatically 14 | reconnectDelay: 3 # time in seconds to delay reconnect try 15 | sendTries: 3 # how often should tried to send a packet 16 | sendDelay: 3 # time in seconds to delay the resend try 17 | 18 | server: # only used if useBroadcast = no 19 | ip: 127.0.0.1 20 | port: 8080 21 | 22 | inputSource: 23 | sdr: 24 | device: 0 25 | frequency: 85M 26 | error: 0 27 | squelch: 1 28 | gain: 100 29 | #fir_size: 0 30 | rtlPath: /usr/local/bin/rtl_fm 31 | lineIn: 32 | card: 1 33 | device: 0 34 | 35 | decoder: 36 | fms: yes 37 | zvei: yes 38 | poc512: yes 39 | poc1200: yes 40 | poc2400: yes 41 | Path: /opt/multimon/multimon-ng 42 | char: DE 43 | -------------------------------------------------------------------------------- /boswatch/utils/paths.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: paths.py 13 | @date: 07.01.2018 14 | @author: Bastian Schroll 15 | @description: Important paths for some functions 16 | """ 17 | import logging 18 | import os 19 | import sys 20 | 21 | logging.debug("- %s loaded", __name__) 22 | 23 | # note searching for root part is not a nice solution atm 24 | ROOT_PATH = os.path.dirname(sys.modules['boswatch'].__file__).replace("\\", "/") + "/../" 25 | 26 | LOG_PATH = ROOT_PATH + "log/" 27 | CONFIG_PATH = ROOT_PATH + "config/" 28 | BIN_PATH = ROOT_PATH + "_bin/" 29 | TEST_PATH = ROOT_PATH + "test/" 30 | 31 | 32 | def fileExist(filePath): 33 | return os.path.exists(filePath) 34 | 35 | 36 | def makeDirIfNotExist(dirPath): 37 | r"""!Checks if an directory is existing and create it if not 38 | 39 | @param dirPath: Path of the directory 40 | @return Path of the directory or False""" 41 | if not os.path.exists(dirPath): 42 | os.mkdir(dirPath) 43 | logging.debug("directory created: %s", dirPath) 44 | return dirPath 45 | -------------------------------------------------------------------------------- /docu/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: BOSWatch3 Core 2 | site_author: Bastian Schroll & BW3 Dev team 3 | 4 | repo_url: https://github.com/BOSWatch/BW3-Core 5 | edit_uri: edit/develop/docu/docs/ 6 | 7 | nav: 8 | # - BW3: index.md 9 | - Quick Start: 10 | - Installation: install.md 11 | - Konfiguration: config.md 12 | - BOSWatch benutzen: usage.md 13 | - Als Service einrichten: service.md 14 | - Informationen: 15 | - Server/Client Prinzip: information/serverclient.md 16 | - Broadcast Service: information/broadcast.md 17 | # - Modul/Plugin Konzept: tbd.md 18 | - Routing Mechanismus: information/router.md 19 | - Changelog: changelog.md 20 | - Module: 21 | - Descriptor: modul/descriptor.md 22 | - Geocoding: modul/geocoding.md 23 | - Mode Filter: modul/mode_filter.md 24 | - Regex Filter: modul/regex_filter.md 25 | - Double Filter: modul/double_filter.md 26 | - Plugins: 27 | - Http: plugin/http.md 28 | - Telegram: plugin/telegram.md 29 | - Divera: plugin/divera.md 30 | - MySQL: plugin/mysql.md 31 | - Entwickler: 32 | - Eigenes Modul/Plugin schreiben: develop/ModulPlugin.md 33 | - BOSWatch Alarmpaket Format: develop/packet.md 34 | - BW3 Quellcode Dokumentation: api/html/index.html 35 | 36 | 37 | use_directory_urls: false 38 | 39 | theme: 40 | name: mkdocs 41 | highlightjs: true 42 | hljs_style: github 43 | 44 | extra_css: 45 | - stylesheets/extra.css 46 | 47 | markdown_extensions: 48 | - attr_list -------------------------------------------------------------------------------- /boswatch/decoder/decoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: decoder.py 13 | @date: 06.01.2018 14 | @author: Bastian Schroll 15 | @description: Some utils for the decoding 16 | """ 17 | import logging 18 | 19 | from boswatch.decoder.fmsDecoder import FmsDecoder 20 | from boswatch.decoder.pocsagDecoder import PocsagDecoder 21 | from boswatch.decoder.zveiDecoder import ZveiDecoder 22 | 23 | logging.debug("- %s loaded", __name__) 24 | 25 | 26 | class Decoder: 27 | 28 | @staticmethod 29 | def decode(data): 30 | r"""!Choose the right decoder and return a bwPacket instance 31 | 32 | @param data: data to decode 33 | @return bwPacket instance""" 34 | data = str(data) 35 | if "FMS" in data: 36 | return FmsDecoder.decode(data) 37 | elif "POCSAG" in data: 38 | return PocsagDecoder.decode(data) 39 | elif "ZVEI" in data: 40 | return ZveiDecoder.decode(data) 41 | else: 42 | logging.warning("no decoder found for: %s", data) 43 | return None 44 | -------------------------------------------------------------------------------- /test/boswatch/test_paths.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: test_paths.py 13 | @date: 22.02.2017 14 | @author: Bastian Schroll 15 | @description: Unittests for BOSWatch. File have to run as "pytest" unittest 16 | """ 17 | # problem of the pytest fixtures 18 | # pylint: disable=redefined-outer-name 19 | import logging 20 | import os 21 | 22 | from boswatch.utils import paths 23 | 24 | 25 | def setup_function(function): 26 | logging.debug("[TEST] %s.%s", function.__module__, function.__name__) 27 | 28 | 29 | def test_fileExists(): 30 | r"""!load a local config file""" 31 | assert paths.fileExist("README.md") 32 | 33 | 34 | def test_fileNotExists(): 35 | r"""!load a local config file""" 36 | assert not paths.fileExist("notFound.txt") 37 | 38 | 39 | def test_makeDirNotExisting(): 40 | r"""!load a local config file""" 41 | assert paths.makeDirIfNotExist("UnItTeSt") 42 | os.removedirs("UnItTeSt") 43 | 44 | 45 | def test_makeDirExisting(): 46 | r"""!load a local config file""" 47 | paths.makeDirIfNotExist("UnItTeSt") 48 | assert paths.makeDirIfNotExist("UnItTeSt") 49 | os.removedirs("UnItTeSt") 50 | -------------------------------------------------------------------------------- /docu/docs/img/client.drawio: -------------------------------------------------------------------------------- 1 | 7Vjbcts2EP0aPdrDi0gpj5FsJ9Nxpm7VadMnD0iuSNQgwYJLU+rXFzeSoijJlyhOMq09o9EeAIvdPdhDQhN/mW8+CFJmn3gCbOI5yWbiX008bx5M5acCtgbwZ74BUkETA7k9sKL/gAUdi9Y0gWowETlnSMshGPOigBgHGBGCN8Npa86Gu5YkhRGwigkbo3/QBDObljfr8Y9A06zd2Q3fmZGctJNtJlVGEt7sQP71xF8KztF8yzdLYKp2bV3Mupsjo11gAgp8zoKmWa0XC//q4Sf8rYohvZn/vrqY29hw2yYMiczfmlxgxlNeEHbdowvB6yIB5dWRVj/nlvNSgq4E/wLErSWT1MgllGHO7ChsKH5Wyy/DmTX/1GZgrauNda6N7Y5xB4LmgCBarECx/bxr7HhSZu9KW62vcfFsPSteixhOVKw9hESkgCfmeWaeKufOBpaaD8BlFmIrJwhgBOnj8LgRe2rTbl5PrPxiuX0Bz9bvI2G13WlE/CMIpPLgv2c0LSSEis1Fi96SCNgdryhSrkYjjshzOYHtDcSyooqeBbF+OmDnBPAaGS1g2TWt4mRNGVtyxoWOx1/PY4hjiVco+APsjERSVQK1IhUkodL9FRXSjdm/UAeyW9X2rKeQjJQq13yTKqm6LAAbLh6qS+WF3yNvdJhHj4YqBWxOktmqnm15q3mhNZteQILAQNmudjjOV2I//CZd/uat6T2zNYPvqjW9A60ZMlQNwWUFvDBFXRWDRfuAmbTDbfh3zduBi0qz815OcKflph9svfwKMagMZdUyASRpvcpMDu0u4VEAkXgaqUpSPCtI/3SQpEjUuwTE8jVDx6w+E4JEd5dsLi8kuZKtIqrKYdwmhGFY9PUxCWT363yinrndFvS8aetkT8Y/LvQXZJTX8q0q58VFkZ7KaXQy9pRlqBtNRhFWJdF920jZHWrESNod/Td+GNws1f951NkNn5RnN3DeUp9dd1TGNxDo1wvt9NwCapfecarFzBIV7hHlunsEGKW3q/Y46MJ4PS3Tb6nMnwgtvm9ZXoFWYyPCssUfAPXtrMMqELIrVZXXHRbxqiEYZ52M6/HKXJjijCrxsVLu0KKs0aj7C2Xwxxep6fRpkZq9qUjN/hsvkcEzXyLProFfRE5wVKo6YfilBjl2UDH2eG0vSvFWXtISfSl6okUiw/Nt1AFSDlLN/s/mqnesl7q73LHb3xl6yZ8Gg16ajXspPNBK86/VSe8OkPX/Zdxexhu5nEFV3efy4ZCf5wCE+2IajMXUOXQCghefAGn2P+mZF5H+d1H/+l8= -------------------------------------------------------------------------------- /docu/docs/plugin/divera.md: -------------------------------------------------------------------------------- 1 | #
Divera 24/7
2 | --- 3 | 4 | ## Beschreibung 5 | Mit diesem Plugin ist es möglich, HTTPS-Anfragen für Alarmierungen an Divera 24/7 zu senden. 6 | Wildcards in den Urls werden automatisch ersetzt. 7 | 8 | ## Unterstütze Alarmtypen 9 | - Fms 10 | - Pocsag 11 | - Zvei 12 | - Msg 13 | 14 | ## Resource 15 | `divera` 16 | 17 | ## Konfiguration 18 | |Feld|Beschreibung|Default| 19 | |----|------------|-------| 20 | |accesskey|Web-API-Schlüssel von Divera24/7 || 21 | |priority|Sonderrechte|false| 22 | |title| Titel der Meldung | s. Beispiel| 23 | |message| Nachrichteninhalt| s. Beispiel| 24 | |ric|Auszulösende RIC in Divera; Gruppen->Alarmierungs-RIC|| 25 | |vehicle|Fahrzeug-Alarmierungs-RIC|| 26 | 27 | **Beispiel:** 28 | ```yaml 29 | - type: plugin 30 | name: Divera Plugin 31 | res: divera 32 | config: 33 | accesskey: API-Key 34 | pocsag: 35 | priority: false 36 | title: "{RIC}({SRIC})\n{MSG}" 37 | message: "{MSG}" 38 | # RIC ist in Divera definiert 39 | ric: Probealarm 40 | fms: 41 | priority: false 42 | title: "{FMS}" 43 | message: "{FMS}" 44 | vehicle: MTF 45 | zvei: 46 | ric: Probealarm 47 | title: "{TONE}" 48 | message: "{TONE}" 49 | priority: false 50 | msg: 51 | priority: false 52 | title: "{MSG}" 53 | message: "{MSG}" 54 | # RIC ist in Divera definiert 55 | ric: Probealarm 56 | 57 | ``` 58 | 59 | --- 60 | ## Modul Abhängigkeiten 61 | - keine 62 | 63 | --- 64 | ## Externe Abhängigkeiten 65 | - asyncio 66 | - aiohttp 67 | - urllib 68 | -------------------------------------------------------------------------------- /config/logger_client.ini: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ____ ____ ______ __ __ __ _____ 3 | # / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 4 | # / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 5 | # / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 6 | #/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 7 | # German BOS Information Script 8 | # by Bastian Schroll 9 | 10 | [loggers] 11 | keys=root 12 | 13 | [logger_root] 14 | handlers=screen,file 15 | level=NOTSET 16 | # NOTSET means: accept all levels (the handlers will filter by their own level) 17 | 18 | [formatters] 19 | keys=simple,complex 20 | 21 | [formatter_simple] 22 | format=%(asctime)s,%(msecs)03d - %(module)-15s [%(levelname)-8s] %(message)s 23 | datefmt=%d.%m.%Y %H:%M:%S 24 | 25 | [formatter_complex] 26 | format=%(asctime)s,%(msecs)03d - %(threadName)-15s %(module)-15s %(funcName)-18s [%(levelname)-8s] %(message)s 27 | datefmt=%d.%m.%Y %H:%M:%S 28 | 29 | [handlers] 30 | keys=file,screen 31 | 32 | [handler_file] 33 | class=handlers.TimedRotatingFileHandler 34 | formatter=complex 35 | level=ERROR 36 | args=(log_filename, 'midnight', 1, 7, 'utf-8') 37 | # explaining args: 38 | # - 'midnight' → rotate daily at midnight, Options: 'S', 'M', 'H', 'D', 'midnight' or 'W0'-'W6' (0=Monday, 6=Sunday) 39 | # - 1 → rotate every 1 "x" (see line above), Options: 1, 2, ..., 31 40 | # - 7 → keep last 7 logs, Options: 1, 2, ..., 31 41 | # - 'utf-8' → encoding of the log file, don't change 42 | 43 | [handler_screen] 44 | class=StreamHandler 45 | formatter=simple 46 | level=DEBUG 47 | args=(sys.stdout,) 48 | -------------------------------------------------------------------------------- /config/logger_server.ini: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ____ ____ ______ __ __ __ _____ 3 | # / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 4 | # / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 5 | # / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 6 | #/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 7 | # German BOS Information Script 8 | # by Bastian Schroll 9 | 10 | [loggers] 11 | keys=root 12 | 13 | [logger_root] 14 | handlers=screen,file 15 | level=NOTSET 16 | # NOTSET means: accept all levels (the handlers will filter by their own level) 17 | 18 | [formatters] 19 | keys=simple,complex 20 | 21 | [formatter_simple] 22 | format=%(asctime)s,%(msecs)03d - %(module)-15s [%(levelname)-8s] %(message)s 23 | datefmt=%d.%m.%Y %H:%M:%S 24 | 25 | [formatter_complex] 26 | format=%(asctime)s,%(msecs)03d - %(threadName)-15s %(module)-15s %(funcName)-18s [%(levelname)-8s] %(message)s 27 | datefmt=%d.%m.%Y %H:%M:%S 28 | 29 | [handlers] 30 | keys=file,screen 31 | 32 | [handler_file] 33 | class=handlers.TimedRotatingFileHandler 34 | formatter=complex 35 | level=ERROR 36 | args=('log/server.log', 'midnight', 1, 7, 'utf-8') 37 | # explaining args: 38 | # - 'midnight' → rotate daily at midnight, Options: 'S', 'M', 'H', 'D', 'midnight' or 'W0'-'W6' (0=Monday, 6=Sunday) 39 | # - 1 → rotate every 1 "x" (see line above), Options: 1, 2, ..., 31 40 | # - 7 → keep last 7 logs, Options: 1, 2, ..., 31 41 | # - 'utf-8' → encoding of the log file, don't change 42 | 43 | [handler_screen] 44 | class=StreamHandler 45 | formatter=simple 46 | level=DEBUG 47 | args=(sys.stdout,) 48 | -------------------------------------------------------------------------------- /docu/docs/img/broadcast.drawio: -------------------------------------------------------------------------------- 1 | 7Vpbb9owFP41PK6K49x4bGm3adrWakzaeJpMYsBdEiNjCuzXzybO1VBCExo2WlVqfGyf2N93bnbTg4No/YGh+ewLDXDYM41g3YO3PdMEwLPFHynZJBIHOolgykigBuWCIfmDldBQ0iUJ8KI0kFMacjIvC30ax9jnJRlijK7KwyY0LL91jqZYEwx9FOrSHyTgs0TqmW4u/4jJdJa+GTj9pCdC6WC1k8UMBXRVEMG7HhwwSnnyFK0HOJTgpbgk897v6c0WxnDM60zof3K/umN0jwfcur8fGeNvv27eKS1PKFyqDavF8k2KAKPLOMBSidGDN6sZ4Xg4R77sXQnOhWzGo1C0gHickDAc0JCy7Vw48Xzs+0K+4Iz+xoWesWdbtlSoFoAZx+u9OwMZXsLQMI0wZxsxRE2wHQWxsjFLbWKVEwYsNWRWIMtMhUgZyTRTneMoHhSUR8BqnhbWAGFvshNWx/fweNIOrMA8N1gdDdYhZmKPGrhi07yMYBmpmMa4AqsSoZBMY9H0BUhCMbyREBIRD65VR0SCQL5mJ2VlUpN3pmFjyyONuYpxEvmkrZYNWqCs6gm2VZOyUzHmaYwNiNzLG2G7faxzwqChcYMDkRFVkzI+o1Mao/Aul1ZQfFxG8xQz0cynfKZ0rnB9xJxv1CC05LTMPV4T/lNqu7JVa1TouV2rF20bm7QRi+0XJsnmqNiXT9u2NrU5TwCRKDxPsgCNLpmPn0u3KoJxxKaYPzewv9tsGA4RJ0/llbRuBEAPtAz7T9uqZkr8M/Xe3by1Gk+B7XTtn0BD/80/2/PP9Ihx0D+dTv1TL9sXYsl8Ob9g1zSMjl0TWDorOA4uL2pWixpgu11T099Hjbw3kNcTiKPLIajqO4KxegS5J0tr+gH59dPa4VRTSGLmUVmsfo46mHogbJh61NQHSuRBLHPavnEFCz+WVzIRCCvUJ+tUSorXTrresq25/bKiJClrirZmlG2vwVWMsa+WvUjXr8bm7l3f1IsZPRbEwbW8zJW4hmixIP7RnqtK1rRIHZXK190la5uFpdGwsCzQY+9gJ5U1DQIV64B2hfU9zqorciuKjHrhozWvr3ED255RGUcYVZZCQCl/5IeixhnkoDGadk1jLPyf4s0YGxijdwbFzX97Zjdh3dBqNSycmqU5qEWkizuzawfD7s/senU4ZhQFPlqc6z8pTsALtM7twG7qdylnUhTmMbGz/J1eNB2OeJ3mb7N6Q+e4L8vf2lWf41wBr5//Oq+bzm3NNgPC5McnpvF98NDLPkYhNL7cGGLCzmOI+5ox5B87A6Q3oofPAHaXMUS7qbSsl8WQajDSFJ04aEC9/PNRnMcK8SSjhRFTFiGpbYU2lxM8ALBL7GTBpGBl2amtYfAQzfyju4Te/NNFePcX -------------------------------------------------------------------------------- /module/filter/modeFilter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: modeFilter.py 13 | @date: 09.03.2019 14 | @author: Bastian Schroll 15 | @description: Filter module for the packet type 16 | """ 17 | import logging 18 | from module.moduleBase import ModuleBase 19 | 20 | # ###################### # 21 | # Custom plugin includes # 22 | 23 | # ###################### # 24 | 25 | logging.debug("- %s loaded", __name__) 26 | 27 | 28 | class BoswatchModule(ModuleBase): 29 | r"""!Filter of specific bwPacket mode""" 30 | def __init__(self, config): 31 | r"""!Do not change anything here!""" 32 | super().__init__(__name__, config) # you can access the config class on 'self.config' 33 | 34 | def onLoad(self): 35 | r"""!Called by import of the plugin""" 36 | pass 37 | 38 | def doWork(self, bwPacket): 39 | r"""!start an run of the module. 40 | 41 | @param bwPacket: A BOSWatch packet instance""" 42 | 43 | for mode in self.config.get("allowed", default=[]): 44 | if bwPacket.get("mode") == mode: 45 | logging.debug("mode is allowed: %s", bwPacket.get("mode")) 46 | return None 47 | logging.debug("mode is denied: %s", bwPacket.get("mode")) 48 | return False 49 | 50 | def onUnload(self): 51 | r"""!Called by destruction of the plugin""" 52 | pass 53 | -------------------------------------------------------------------------------- /boswatch/network/netCheck.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: netCheck.py 13 | @date: 21.09.2018 14 | @author: Bastian Schroll 15 | @description: Worker to check internet connection 16 | """ 17 | import logging 18 | from urllib.request import urlopen 19 | 20 | logging.debug("- %s loaded", __name__) 21 | 22 | 23 | class NetCheck: 24 | r"""!Worker class to check internet connection""" 25 | 26 | def __init__(self, hostname="https://www.google.com/", timeout=1): 27 | r"""!Create a new NetCheck instance 28 | 29 | @param hostname: host against connection check is running ("https://www.google.com/") 30 | @param timeout: timeout for connection check in sec. (1)""" 31 | self._hostname = hostname 32 | self._timeout = timeout 33 | self.connectionState = False 34 | self.checkConn() # initiate a first check 35 | 36 | def checkConn(self): 37 | r"""!Check the connection 38 | 39 | @return True or False""" 40 | try: 41 | urlopen(self._hostname, timeout=self._timeout) 42 | logging.debug("%s is reachable", self._hostname) 43 | self.connectionState = True 44 | return True 45 | except: # todo find right exception type 46 | logging.warning("%s is not reachable", self._hostname) 47 | self.connectionState = False 48 | return False 49 | -------------------------------------------------------------------------------- /docu/docs/information/serverclient.md: -------------------------------------------------------------------------------- 1 | #
Server/Client Prinzip
2 | 3 | BOSWatch 3 wurde als Server/Client Anwedung entwickelt. 4 | 5 | Dies ermöglicht es, mehrere Empfangsstationen an einer Auswerte- und Verteilereinheit zu bündeln. 6 | 7 | --- 8 | ## BOSWatch Client 9 | 10 | Der **BOSWatch Client** übernimmt den Empfang und die Dekodierung der Daten. Anschließend werden die Daten mittels der implemetierten 11 | Dekoder ausgewertet und in ein sogenanntes bwPacket verpackt. 12 | 13 | Dieses Paket wird anschließend in einer Sende-Queue abgelegt. Nun werden Pakete aus der Queue an den BOSWatch Server per TCP-Socket 14 | gesendet. Der Ansatz, Pakete statt dem direkten versenden vorher in einer Queue zwischen zu speichern, verhindert den Verlust von 15 | Paketen, sollte die Verbindung zum Server einmal abreisen. Nach einer erfolgreichen Wiederverbdingun können die wartenden Pakete nun 16 | nachträglich an den Server übermittelt werden. 17 | 18 | Dabei überwacht der Client selbstständig die benötigten Programme zum Empfang der Daten und startet diese bei einem Fehler ggf. neu. 19 | 20 | ![Client-Prinzip](../img/client.png){ .center } 21 | 22 | --- 23 | ## BOSWatch Server 24 | 25 | Nachdem die Daten vom Clienten über die TCP-Socket Verbindung empfangen wurden, übernimmt der **BOSWatch Server** die weitere 26 | Verarbeitung der Daten. 27 | 28 | Auch hier werden die empfangenen Daten in From von bwPacket's in einer Queue abelegt um zu gewährleisten, das auch während einer länger 29 | dauernden Plugin Ausführung alle Pakete korrekt empfangen werden können und es zu keinen Verlusten kommt. 30 | Die Verarbeitung der Pakete geschieht anschließend in sogenannten Routern, welche aufgrund ihres Umfangs jedoch in einem eigenen Kapitel 31 | erklärt werden. Diese steuern die Verteilung der Daten an die einzelnen Plugins. 32 | 33 | ![Server-Prinzip](../img/server.png){ .center } -------------------------------------------------------------------------------- /module/template_module.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: template_module.py 13 | @date: 01.03.2019 14 | @author: Bastian Schroll 15 | @description: Template Module File 16 | """ 17 | import logging 18 | from module.moduleBase import ModuleBase 19 | 20 | # ###################### # 21 | # Custom plugin includes # 22 | 23 | # ###################### # 24 | 25 | logging.debug("- %s loaded", __name__) 26 | 27 | 28 | class BoswatchModule(ModuleBase): 29 | r"""!Description of the Module""" 30 | def __init__(self, config): 31 | r"""!Do not change anything here!""" 32 | super().__init__(__name__, config) # you can access the config class on 'self.config' 33 | 34 | def onLoad(self): 35 | r"""!Called by import of the plugin 36 | Remove if not implemented""" 37 | pass 38 | 39 | def doWork(self, bwPacket): 40 | r"""!start an run of the module. 41 | 42 | @param bwPacket: A BOSWatch packet instance""" 43 | if bwPacket.get("mode") == "fms": 44 | pass 45 | elif bwPacket.get("mode") == "zvei": 46 | pass 47 | elif bwPacket.get("mode") == "pocsag": 48 | pass 49 | elif bwPacket.get("mode") == "msg": 50 | pass 51 | 52 | return bwPacket 53 | 54 | def onUnload(self): 55 | r"""!Called by destruction of the plugin 56 | Remove if not implemented""" 57 | pass 58 | -------------------------------------------------------------------------------- /test/boswatch/test_packet.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: test_packet.py 13 | @date: 12.12.2017 14 | @author: Bastian Schroll 15 | @description: Unittests for BOSWatch. File have to run as "pytest" unittest 16 | """ 17 | # problem of the pytest fixtures 18 | # pylint: disable=redefined-outer-name 19 | import logging 20 | import pytest 21 | 22 | from boswatch.packet import Packet 23 | 24 | 25 | def setup_function(function): 26 | logging.debug("[TEST] %s.%s", function.__module__, function.__name__) 27 | 28 | 29 | @pytest.fixture() 30 | def buildPacket(): 31 | r"""!Build a BOSWatch packet and serve it to each test""" 32 | return Packet() 33 | 34 | 35 | def test_createPacket(buildPacket): 36 | r"""!Create a packet""" 37 | assert buildPacket != "" 38 | 39 | 40 | def test_copyPacket(buildPacket): 41 | r"""!Copy a packet to an new instance""" 42 | bwCopyPacket = Packet(buildPacket.__str__()) 43 | assert bwCopyPacket != "" 44 | 45 | 46 | def test_getPacketString(buildPacket): 47 | r"""!get the intern packet dict as string""" 48 | assert type(buildPacket.__str__()) is str 49 | assert buildPacket.__str__() != "" 50 | 51 | 52 | def test_getNotSetField(buildPacket): 53 | r"""!try to get a not set field""" 54 | assert not buildPacket.get("testfield") 55 | 56 | 57 | def test_setGetField(buildPacket): 58 | r"""!set and get a field""" 59 | buildPacket.set("testField", "test") 60 | assert buildPacket.get("testField") == "test" 61 | -------------------------------------------------------------------------------- /docu/docs/develop/packet.md: -------------------------------------------------------------------------------- 1 | #
BOSWatch Paket Format
2 | 3 | Ein BOSWatch Datenpaket wird in einem Python Dict abgebildet. In der nachfolgenden Tabelle sind die genutzten Felder abgebildet. 4 | 5 | --- 6 | ## Allgemeine Informationen 7 | |Feldname|FMS|POCSAG|ZVEI|MSG|Wildcard|Beschreibung| 8 | |--------|:-:|:----:|:--:|:-:|--------|------------| 9 | |serverName|X|X|X|X|`{SNAME}`|Name der BOSWatch Server Instanz| 10 | |serverVersion|X|X|X|X|`{SVERS}`|| 11 | |serverBuildDate|X|X|X|X|`{SDATE}`|| 12 | |serverBranch|X|X|X|X|`{SBRCH}`|| 13 | |clientName|X|X|X|X|`{CNAME}`|Name der BOSWatch Client Instanz| 14 | |clientIP|X|X|X|X|`{CIP}`|| 15 | |clientVersion|X|X|X|X|`{CVERS}`|| 16 | |clientBuildDate|X|X|X|X|`{CDATE}`|| 17 | |clientBranch|X|X|X|X|`{CBRCH}`|| 18 | |inputSource|X|X|X|X|`{INSRC}`|(sdr, audio)| 19 | |timestamp|X|X|X|X|`{TIMES}`|| 20 | |frequency|X|X|X|X|`{FREQ}`|| 21 | |mode|X|X|X|X|`{MODE}`|(fms, pocsag, zvei, msg)| 22 | 23 | --- 24 | ## Speziell für POCSAG 25 | |Feldname|FMS|POCSAG|ZVEI|MSG|Wildcard|Beschreibung| 26 | |--------|:-:|:----:|:--:|:-:|--------|------------| 27 | |bitrate||X|||`{BIT}`|| 28 | |ric||X|||`{RIC}`|| 29 | |subric||X|||`{SRIC}`|(1, 2, 3, 4)| 30 | |subricText||X|||`{SRICT}`|(a, b, c, d)| 31 | |message||X||X|`{MSG}`|Kann außerdem für ein Message Paket genutzt werden| 32 | 33 | --- 34 | ## Speziell für ZVEI 35 | |Feldname|FMS|POCSAG|ZVEI|MSG|Wildcard|Beschreibung| 36 | |--------|:-:|:----:|:--:|:-:|--------|------------| 37 | |tone|||X||`{TONE}`|5-Ton Sequenz nach ZVEI| 38 | 39 | --- 40 | ## Speziell für FMS 41 | |Feldname|FMS|POCSAG|ZVEI|MSG|Wildcard|Beschreibung| 42 | |--------|:-:|:----:|:--:|:-:|--------|------------| 43 | |fms|X||||`{FMS}`|| 44 | |service|X||||`{SERV}`|| 45 | |country|X||||`{COUNT}`|| 46 | |location|X||||`{LOC}`|| 47 | |vehicle|X||||`{VEC}`|| 48 | |status|X||||`{STAT}`|| 49 | |direction|X||||`{DIR}`|| 50 | |directionText|X||||`{DIRT}`|(Fhz->Lst, Lst->Fhz)| 51 | |tacticalInfo|X||||`{TACI}`|(I, II, III, IV)| 52 | -------------------------------------------------------------------------------- /boswatch/utils/misc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: misc.py 13 | @date: 11.03.2019 14 | @author: Bastian Schroll 15 | @description: Some misc functions 16 | """ 17 | import logging 18 | from boswatch.utils import version 19 | 20 | logging.debug("- %s loaded", __name__) 21 | 22 | 23 | def addClientDataToPacket(bwPacket, config): 24 | r"""!Add the client information to the decoded data 25 | 26 | This function adds the following data to the bwPacket: 27 | - clientName 28 | - clientVersion 29 | - clientBuildDate 30 | - clientBranch 31 | - inputSource 32 | - frequency""" 33 | logging.debug("add client data to bwPacket") 34 | bwPacket.set("clientName", config.get("client", "name")) 35 | bwPacket.set("clientVersion", version.client) 36 | bwPacket.set("clientBuildDate", version.date) 37 | bwPacket.set("clientBranch", version.branch) 38 | bwPacket.set("inputSource", config.get("client", "inputSource")) 39 | bwPacket.set("frequency", config.get("inputSource", "sdr", "frequency")) 40 | 41 | 42 | def addServerDataToPacket(bwPacket, config): 43 | r"""!Add the server information to the decoded data 44 | 45 | This function adds the following data to the bwPacket: 46 | - serverName 47 | - serverVersion 48 | - serverBuildDate 49 | - serverBranch""" 50 | logging.debug("add server data to bwPacket") 51 | bwPacket.set("serverName", config.get("server", "name")) 52 | bwPacket.set("serverVersion", version.server) 53 | bwPacket.set("serverBuildDate", version.date) 54 | bwPacket.set("serverBranch", version.branch) 55 | -------------------------------------------------------------------------------- /boswatch/decoder/zveiDecoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: zvei.py 13 | @date: 05.01.2018 14 | @author: Bastian Schroll 15 | @description: Decoder class for zvei 16 | """ 17 | 18 | import logging 19 | import re 20 | 21 | from boswatch.packet import Packet 22 | 23 | logging.debug("- %s loaded", __name__) 24 | 25 | 26 | class ZveiDecoder: 27 | r"""!ZVEI decoder class 28 | 29 | This class decodes ZVEI data. 30 | First step is to validate the data and _check if the format is correct. 31 | After that the double-tone-sign 'E' is replaced. 32 | In the last step a valid BOSWatch packet is created and returned""" 33 | 34 | @staticmethod 35 | def decode(data): 36 | r"""!Decodes ZVEI 37 | 38 | @param data: ZVEI for decoding 39 | @return BOSWatch ZVEI packet or None""" 40 | if re.search("[0-9E]{5}", data[7:12]): 41 | logging.debug("found valid ZVEI") 42 | 43 | bwPacket = Packet() 44 | bwPacket.set("mode", "zvei") 45 | bwPacket.set("tone", ZveiDecoder._solveDoubleTone(data[7:12])) 46 | 47 | return bwPacket 48 | 49 | logging.warning("no valid ZVEI") 50 | return None 51 | 52 | @staticmethod 53 | def _solveDoubleTone(data): 54 | r"""!Remove the doubleTone sign (here its the 'E') 55 | 56 | @param data: ZVEI for double tone sign replacement 57 | @return Double Tone replaced ZVEI""" 58 | if "E" in data: 59 | data_old = data 60 | for i in range(1, len(data)): 61 | if data[i] == "E": 62 | data = data.replace("E", data[i - 1], 1) 63 | logging.debug("solve doubleTone: %s -> %s", data_old, data) 64 | return data 65 | -------------------------------------------------------------------------------- /docu/docs/modul/regex_filter.md: -------------------------------------------------------------------------------- 1 | #
Regex Filter
2 | --- 3 | 4 | ## Beschreibung 5 | Mit diesem Modul ist es möglich, komplexe Filter basierend auf Regulären Ausdrücken (Regex) anzulegen. 6 | Für einen Filter können beliebig viele Checks angelegt werden, welche Felder eines BOSWatch Pakets mittels Regex prüfen. 7 | 8 | Folgendes gilt: 9 | 10 | - Die Filter werden nacheinander abgearbeitet 11 | - Innerhalb des Filters werden die Checks nacheinander abgearbeitet 12 | - Sobald ein einzelner Check fehlschlägt ist der ganze Filter fehlgeschlagen 13 | - Sobald ein Filter mit all seinen Checks besteht, wird mit der Ausführung des Routers fortgefahren 14 | - Sollten alle Filter fehlschlagen wird die Ausführung des Routers beendet 15 | 16 | Vereinfacht kann man sagen, dass einzelnen Router ODER-verknüpft und die jeweiligen Checks UND-verknüpft sind. 17 | 18 | ## Unterstütze Alarmtypen 19 | - Fms 20 | - Pocsag 21 | - Zvei 22 | - Msg 23 | 24 | ## Resource 25 | `filter.regexFilter` 26 | 27 | ## Konfiguration 28 | |Feld|Beschreibung|Default| 29 | |----|------------|-------| 30 | |name|Beliebiger Name des Filters|| 31 | |checks|Liste der einzelnen Checks innerhalb des Filters|| 32 | 33 | #### `checks:` 34 | |Feld|Beschreibung|Default| 35 | |----|------------|-------| 36 | |field|Name des Feldes innerhalb des BOSWatch Pakets welches untersucht werden soll|| 37 | |regex|Regulärer Ausdruck (Bei Sonderzeichen " " verwenden)|| 38 | 39 | **Beispiel:** 40 | ```yaml 41 | - type: module 42 | res: filter.regexFilter 43 | config: 44 | - name: "Zvei filter" 45 | checks: 46 | - field: tone 47 | regex: "65[0-9]{3}" # all zvei with starting 65 48 | - name: "FMS Stat 3" 49 | checks: 50 | - field: mode 51 | regex: "fms" # check if mode is fms 52 | - field: status 53 | regex: "3" # check if status is 3 54 | - name: "Allowed RICs" 55 | checks: 56 | - field: ric 57 | regex: "(0000001|0000002|0000003)" # check if RIC is in the list 58 | ``` 59 | 60 | --- 61 | ## Modul Abhängigkeiten 62 | - keine 63 | 64 | --- 65 | ## Externe Abhängigkeiten 66 | - keine 67 | 68 | --- 69 | ## Paket Modifikationen 70 | - keine 71 | 72 | --- 73 | ## Zusätzliche Wildcards 74 | - keine 75 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ develop, master ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ develop ] 9 | schedule: 10 | # ┌───────────── minute (0 - 59) 11 | # │ ┌───────────── hour (0 - 23) 12 | # │ │ ┌───────────── day of the month (1 - 31) 13 | # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) 14 | # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) 15 | # │ │ │ │ │ 16 | # │ │ │ │ │ 17 | # │ │ │ │ │ 18 | # * * * * * 19 | - cron: '33 03 * * 5' 20 | 21 | jobs: 22 | CodeQL-Build: 23 | # CodeQL runs on ubuntu-latest, windows-latest, and macos-latest 24 | runs-on: ubuntu-latest 25 | 26 | permissions: 27 | # required for all workflows 28 | security-events: write 29 | 30 | # only required for workflows in private repositories 31 | actions: read 32 | contents: read 33 | 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v4 37 | 38 | # Initializes the CodeQL tools for scanning. 39 | - name: Initialize CodeQL 40 | uses: github/codeql-action/init@v3 41 | # Override language selection by uncommenting this and choosing your languages 42 | # with: 43 | # languages: go, javascript, csharp, python, cpp, java 44 | 45 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 46 | # If this step fails, then you should remove it and run the build manually (see below). 47 | - name: Autobuild 48 | uses: github/codeql-action/autobuild@v3 49 | 50 | # ℹ️ Command-line programs to run using the OS shell. 51 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 52 | 53 | # ✏️ If the Autobuild fails above, remove it and uncomment the following 54 | # three lines and modify them (or add more) to build your code if your 55 | # project uses a compiled language 56 | 57 | #- run: | 58 | # make bootstrap 59 | # make release 60 | 61 | - name: Perform CodeQL Analysis 62 | uses: github/codeql-action/analyze@v3 63 | -------------------------------------------------------------------------------- /docu/docs/img/server.drawio: -------------------------------------------------------------------------------- 1 | 7Vpfc9o4EP80zNw9hDEWBvIYIOlNJ5n2SufaPHWELRtdhOXKcoH79LeyJf8HnIYUbnJkxpFW0nql3f1ptXIPzdbbdwJHqwfuEdazLW/bQ/OebU+cITwVYZcRhqqmCIGgXkYaFIQF/YdooqWpCfVIXOkoOWeSRlWiy8OQuLJCw0LwTbWbz1n1rREOSIOwcDFrUr9QT670tOxxQf+D0GBl3jwYXWcta2w665nEK+zxTYmEbntoJjiXWWm9nRGm1s6sSzbubk9rLpggoewyYLNZ+NMpmj+9l59jlwR3k78WV0iz+YFZomespZU7swSCJ6FHFBerh6abFZVkEWFXtW5A50BbyTWD2gCKmNEghDIjPkg19SljM864SFkh31F/QI+l4E+k1DJKf2oED2WJnv2A3pyuEZ0ISbYlkp7+O8LXRIoddNGt11oT2hQHY13fFIodWZq2KinVjMPaloKcc7HcUNAr/ozVt53GYhMPzE9XuZArHvAQs9uCOq2qo+hzz3mklfA3kXKnfQknkldVBAsodl/V+L5jqo+aXVqZbyu1na7t1UDME+GSA/PUGCCxCIg8tB66o1qEgwoVhGFJf1R9tE096dAbIfCu1CHiNJRxifNHRSjsZGQ5VUNxap5V6z+YHOwPhUyCwlDyqfy87QxbHHfEpPYgKAcy1VlGW9YJWaeS5Y2+J9w0XMWp7dxAh8Ew2haNhssDpqFS6EoQ7BmWMI22VwO58falOE6JIxx2khC1STinADF0mUiSyklSy3WfiNK7kpsnwSpvgZqkYaBQm7grHNJ4XZI+E+SYuI3ZvwRFG/Bopb8moN7N1N9p4NGxq/hoOJTgMYfMMjwOrNfCx8F58HFL5ddS+bGASqgV4KgqBhsLTK0gagGwJ8dUpyOmnhxSX6RRZy9q5RjxZ0KgrRU8arYAEVWkiu6OUVC5OO5Xy8w27pc5ATAhSC3mQyKBC9nngEuIZJ0WB/QnLnHd0zjgsOaAqOmAoxb/m7ya+6Hzhie/ypWuO7rSwL4oX7o+Hror26NwmLnRUblUOpga6j1eEvaRx1RSrlqXXEq+hg6s1uDCkqbuZaL7nFDSG88caJYfxA55y37/CgT2KLCfUwFssveHyozyUeYcZiuKhoD1NlDHz35I5IaLp7i/geGMxPG3NZxI16fxz0ltf5z0neYO2XaAMPHh6T10/P8Gecxbj7q1c1FebcQ+T2D/ibhETfGiY/uykOrpYYlVbkcAeNnWkscbLF0V3btMIYnSUugpK0pUmaaPEIxa0eHxvRZwvJV4P8+xXUy8b7Jqh7a0X4BvP485dlfMsdFFgY59VtB54F4Ce/VFYs0sHQxeqOId2+LqASEF9XcGfurv1WikEyRF4uENIgyqJ9ImzYxrK8SMXw1h0DkN/SNLAhpepqHfhl6k05rcr2ytffi/wGq/nSn2JN1LU0eIlVZWWeaMsPgtbqJObRO9ABMf/dc30c7XBZd1Hrefl5Lv7K6tUNJAqE9w/lZb1H4M6eyVp4aWBxzigMSN/dDyGd8oqsnGr00gYAL2Bl6+FVjJY+yLgRXUFiLWYSX0btSdO9RchuOYur3WxIHVt0224LHUdCxz0B9X7ivzZOHJUwfdw/iuCFRSm9OiNUN73r1my8VlxWgQqhlDNnE9qLCHBp/h9RFG2cI0GD33ohUNJ5X3mAzbXrkQOtT/xRet127kf/nw+D6kXH4bPM7nSCRXbV9I1LA3PxA0Iv0DgVw+KA2lsKcALySbPJ9R8y0ADln1JkEAePEy7aDsXa829HamPWfea3yMUc8Gr6nnpZt9mu+d5lch5YRt+sudqQFg+Yc3Wo5e/rlL2e8OAMpeBLT6gxEaVUHwJE5yNbSrXEdVDtz3YyJrdnUSS2qLDw4gKBVuqp2XA6h3R1k1LqsB1ktg8lxgV8vGo/rO1xXs6iCEht3A7lRW0eFKNFb3NtlnCodDvAJdfiP9QB3c/BRdsvxFdlCLEhHxmMS/nwZiIPDP4r/K51/5BVH7DRSEcS5I8zk9S4BfHMCXhmHuz2eOrXZFlmOmtgvU+u7WIWaCavHlXmYJxeeP6PZf -------------------------------------------------------------------------------- /boswatch/packet.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: packet.py 13 | @date: 06.01.2018 14 | @author: Bastian Schroll 15 | @description: Class for a BOSWatch data packet 16 | """ 17 | import logging 18 | import time 19 | 20 | logging.debug("- %s loaded", __name__) 21 | 22 | 23 | class Packet: 24 | r"""!Class implementation of an BOSWatch packet""" 25 | 26 | def __init__(self, bwPacket=None): 27 | r"""!Build a new BOSWatch packet or copy existing data in it 28 | 29 | @param bwPacket: Existing data to copy""" 30 | if bwPacket is None: 31 | logging.debug("create new bwPacket") 32 | self._packet = {"timestamp": time.time()} 33 | else: 34 | logging.debug("create bwPacket from string") 35 | self._packet = eval(str(bwPacket.strip())) 36 | 37 | def __str__(self): 38 | r"""!Return the intern _packet dict as string""" 39 | return str(self._packet) 40 | 41 | def set(self, fieldName, value): 42 | r"""!Set a field in the intern _packet dict 43 | 44 | @param fieldName: Name of the data to set 45 | @param value: Value to set""" 46 | self._packet[fieldName] = str(value) 47 | 48 | def get(self, fieldName): 49 | r"""!Returns the value from a single field. 50 | If field not existing `None` is returned 51 | 52 | @param fieldName: Name of the field 53 | @return Value or None""" 54 | try: 55 | return str(self._packet[fieldName]) 56 | except: 57 | logging.warning("field not found: %s", fieldName) 58 | return None 59 | 60 | def printInfo(self): 61 | r"""!Print a info message to the log on INFO level. 62 | Contains the most useful info about this packet. 63 | @todo not complete yet - must be edit to print nice formatted messages on console 64 | """ 65 | logging.info("[%s]", self.get("mode")) 66 | -------------------------------------------------------------------------------- /plugin/template_plugin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: template_plugin.py 13 | @date: 14.01.2018 14 | @author: Bastian Schroll 15 | @description: Template Plugin File 16 | """ 17 | import logging 18 | from plugin.pluginBase import PluginBase 19 | 20 | # ###################### # 21 | # Custom plugin includes # 22 | 23 | # ###################### # 24 | 25 | logging.debug("- %s loaded", __name__) 26 | 27 | 28 | class BoswatchPlugin(PluginBase): 29 | r"""!Description of the Plugin""" 30 | def __init__(self, config): 31 | r"""!Do not change anything here!""" 32 | super().__init__(__name__, config) # you can access the config class on 'self.config' 33 | 34 | def onLoad(self): 35 | r"""!Called by import of the plugin 36 | Remove if not implemented""" 37 | pass 38 | 39 | def setup(self): 40 | r"""!Called before alarm 41 | Remove if not implemented""" 42 | pass 43 | 44 | def fms(self, bwPacket): 45 | r"""!Called on FMS alarm 46 | 47 | @param bwPacket: bwPacket instance 48 | Remove if not implemented""" 49 | pass 50 | 51 | def pocsag(self, bwPacket): 52 | r"""!Called on POCSAG alarm 53 | 54 | @param bwPacket: bwPacket instance 55 | Remove if not implemented""" 56 | pass 57 | 58 | def zvei(self, bwPacket): 59 | r"""!Called on ZVEI alarm 60 | 61 | @param bwPacket: bwPacket instance 62 | Remove if not implemented""" 63 | pass 64 | 65 | def msg(self, bwPacket): 66 | r"""!Called on MSG packet 67 | 68 | @param bwPacket: bwPacket instance 69 | Remove if not implemented""" 70 | pass 71 | 72 | def teardown(self): 73 | r"""!Called after alarm 74 | Remove if not implemented""" 75 | pass 76 | 77 | def onUnload(self): 78 | r"""!Called by destruction of the plugin 79 | Remove if not implemented""" 80 | pass 81 | -------------------------------------------------------------------------------- /docu/docs/img/router.drawio: -------------------------------------------------------------------------------- 1 | 7Vxbc+I2FP41zLQPYWz5AnkEkmx3ujtLS6d7eekIW7bVCsuVRQL99ZV8wRfsrAlgkQ15wTqWbEnnfOeTjk48MGarzTsGo+AjdREZAM3dDIy7AQC6qY3Fj5RsU8nIvE0FPsNuVqkQLPB/KBNqmXSNXRRXKnJKCcdRVejQMEQOr8ggY/SpWs2jpPrWCPpoT7BwINmXfsYuD1LpGIwK+S8I+0H+Zt3OxreCeeVsJHEAXfpUEhn3A2PGKOXp1WozQ0ROXj4vabuHlru7jjEU8i4NHh9mC+RObr/huQbnwa/2/ca8sbJxxHybjxi5YgKyImU8oD4NIbkvpFNG16GL5GM1USrqfKA0EkJdCP9GnG8zbcI1p0IU8BXJ7qIN5l+y5vL6q7wejqyseLcp3bvb5oWQs+2XciFtZuXFollSytulA5Sjap24TBTTNXPQM7Nl5BYImY/4MxXtnX4FMBBdIdEh0Y4hAjl+rHYEZhbq7+oVShQXmR4P0Gney0dI1tmrBsAmor/TOIJhRdv2v2tpf1OPhvwmTvQ1ERV0M9oUN8WVn/0mT1nmAgH3NdlJWUmMHjDhSEi0n779ef/+5/1KuUQMcdkgS3uai2smWhigtKanAHO0iGCiuifhhqrG5mFCZpRQlrQ1XIjGniPngjP6DyrdsZ0xWnq79z0ixtHmeZvZV3HWwM5An3k9My8/FT5E+MZUFpT8Ry47uVHk/lQR0IdWCep6R5wX0P5aQfbZcd4V5kAlzNtRvocnie6Xw/53uhZQ1kvgTJ/3HI5PCFjP84DTCFjXXtqWfRrAmlXA6k2AtXsFrFpmfl2ABR0BqyslZqCYl+9Q7DAcccreOB3v1uTq6PhWJbr1QXndDS5/3d0V3i1W0BneSdMJY3BbqhBRHPK49OS5FBTGBcZV6zKs2kbsO/X1sVazp7QHhXXthnKE7xlf6eTk9gZMlXRi9EUnDkw6k64DweVwRz8rwxp3GKZq7rguDLsj2e7KHCqBbO8Befpp8RlyJxBSI1E1ZJKBNEggW0k7g6FLcOgnZshlNR6ggVzVpKjDrVhOnpAi+Tuw3X/GDvAeo6s05uph/zhInwCgta2b1XHnNjoXPsE11HJAqKVrrEW3VEJU7znaAgY/dLTFAJcWbgHgCtruoO0ab2kzg55A2x5wOQtojR8btLZ2aaC1LKWgrURRup5e6tUoyqjHMIpudYStcSRqXxRGqXMC+E4YpV7fGPUQRsmn8ELOVh8+Lt7c0aphXlww9xpbO8AJdd2SH+uEjlPp/p68X5y7dL0kO6S/LYTXT2vGqgFuqF1nvC6Ag647ekMpwkH7jl4Fk88/zd4ck9v1FZ9yJgfXY9lDkG52RHq6Ee97Q2HVk/D05zcU9fq9nMua12DxGQzOUBp3yrvZ88mscTn8oeRk1tSU00dv0YE5Wfs4bFhUbBe/fbggOxg7qNkOlmPLtLTT2MFeRED9huH1RQQUxiWNzscJavNt2o8Trug+H7rruwT1CTiG/erQrTCXrmsynXHsJuE4nfaWTdcG7j8QQT6Dq7eO7wtg71vVtrD4uHhzZlDL4lIeCzJNpV5+UI4FvexsucclnNnVyyv9Bxyz3cl3zv+wm4A9yfIyBaT+QfI5WGZrZmmSfBuVczNfnnrS+Or5p9li8u6gzJOWqq0uQzvcZXjIbtn/j26X2olcRt1n2Naey9iRST/5KE07hNq0xgGM5CVeJV8eKE+inAzsQDIh2A+FjEufsZN+gEtE5jTGHFN5d0k5F/ZlTIm8MRWW5ycqq4ZhxJ+okrxsEkfpFxLk9MO84OGNVPI0689dwLn8tMJEzgR4cNzQHOIk0VcYAxs60qIfXMihzBAW8jj9daRnQTceQ+gmF+u6KW3sxmE0jv8Cln2jg/EwCv0TqF6vLRqshtgPaFA9OJvqm1zLVfVnUL1ZS0MDDWlo/aq+PVfguKzC93KpyYOESxwYSxKhIdkO8mz/KFk9xk3cUmwmtCYOgKHb1CqLLzQ2ecLJTCxR0hlCkDsUV5NESEV/WKJEHqSdDZ3kOzBpP701yyswYS5xnP7nAvV2NWCdP5fIgetkxGmFlTxDbWTR8jFqE9mlVBwLQEXpa9MHsiRkPuxKhsJceRWxVWYLaYhqNJiJYIZogjzeAPQVdt1k5dhEsFUKPgVj1iKlprVPmVYTeG4PB48oFh+3SY/Lik8EGff/Aw== -------------------------------------------------------------------------------- /boswatch/decoder/fmsDecoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: fms.py 13 | @date: 06.01.2018 14 | @author: Bastian Schroll 15 | @description: Decoder class for fms 16 | """ 17 | 18 | import logging 19 | import re 20 | 21 | from boswatch.packet import Packet 22 | 23 | logging.debug("- %s loaded", __name__) 24 | 25 | 26 | class FmsDecoder: 27 | r"""!FMS decoder class 28 | 29 | This class decodes FMS data. 30 | First step is to validate the data and _check if the format is correct. 31 | In the last step a valid BOSWatch packet is created and returned""" 32 | 33 | @staticmethod 34 | def decode(data): 35 | r"""!Decodes FMS 36 | 37 | @param data: FMS for decoding 38 | @return BOSWatch FMS packet or None""" 39 | if "CRC correct" in data: 40 | service = data[19] 41 | country = data[36] 42 | location = data[61:63] 43 | vehicle = data[72:76] 44 | status = data[84] 45 | direction = data[101] 46 | directionText = data[103:110] 47 | tacticalInfo = data[114:117] 48 | fms_id = service + country + location + vehicle + status + direction 49 | 50 | if re.search("[0-9a-f]{8}[0-9a-f][01]", fms_id): 51 | logging.debug("found valid FMS") 52 | 53 | bwPacket = Packet() 54 | bwPacket.set("mode", "fms") 55 | bwPacket.set("fms", fms_id) 56 | bwPacket.set("service", service) 57 | bwPacket.set("country", country) 58 | bwPacket.set("location", location) 59 | bwPacket.set("vehicle", vehicle) 60 | bwPacket.set("status", status) 61 | bwPacket.set("direction", direction) 62 | bwPacket.set("directionText", directionText) 63 | bwPacket.set("tacticalInfo", tacticalInfo) 64 | 65 | return bwPacket 66 | 67 | logging.warning("no valid FMS") 68 | return None 69 | logging.warning("CRC Error") 70 | return None 71 | -------------------------------------------------------------------------------- /docu/docs/plugin/telegram.md: -------------------------------------------------------------------------------- 1 | #
Telegram
2 | --- 3 | 4 | ## Beschreibung 5 | Dieses Plugin ermöglicht das Versenden von Telegram-Nachrichten für verschiedene Alarmierungsarten. 6 | 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. 7 | 8 | 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. 9 | 10 | ## Unterstütze Alarmtypen 11 | - FMS 12 | - POCSAG 13 | - ZVEI 14 | - MSG 15 | 16 | ## Resource 17 | `telegram` 18 | 19 | ## Konfiguration 20 | 21 | |Feld|Beschreibung|Default| 22 | |----|------------|-------| 23 | |botToken|Der Api-Key des Telegram-Bots|-| 24 | |chatIds|Liste mit Chat-Ids der Empfängers / der Empfänger-Gruppen|-| 25 | |startup_message|Nachricht beim erfolgreichen Initialisieren des Plugins|leer| 26 | |message_fms|Formatvorlage für FMS-Alarm|`{FMS}`| 27 | |message_pocsag|Formatvorlage für POCSAG|`{RIC}({SRIC})\n{MSG}`| 28 | |message_zvei|Formatvorlage für ZVEI|`{TONE}`| 29 | |message_msg|Formatvorlage für MSG-Nachricht|-| 30 | |max_retries|Anzahl Wiederholungsversuche bei Fehlern|5| 31 | |initial_delay|Initiale Wartezeit bei Wiederholungsversuchen|2 [Sek.]| 32 | |max_delay|Maximale Retry-Verzögerung|300 [Sek.]| 33 | |parse_mode|Formatierung ("HTML" oder "MarkdownV2"), Case-sensitive!|leer| 34 | 35 | **Beispiel:** 36 | ```yaml 37 | - type: plugin 38 | name: Telegram Plugin 39 | res: telegram 40 | config: 41 | message_pocsag: | 42 | POCSAG Alarm: 43 | RIC: {RIC} ({SRIC}) 44 | {MSG} 45 | parse_mode: "HTML" 46 | startup_message: "Server up and running!" 47 | botToken: "BOT_TOKEN" 48 | chatIds: 49 | - "CHAT_ID" 50 | ``` 51 | 52 | Hinweis: 53 | Über parse_mode kannst du Telegram-Formatierungen verwenden: 54 | 55 | - HTML: `fett`, `kursiv`, `unterstrichen`, `durchgestrichen`, ... 56 | - MarkdownV2: `**fett**`, `__unterstrichen__`, `_italic \*text_` usw. (Escape-Regeln beachten) 57 | 58 | Block-Strings (|) eignen sich perfekt für mehrzeilige Nachrichten und vermeiden Escape-Zeichen wie \n 59 | 60 | --- 61 | ## Modul Abhängigkeiten 62 | OPTIONAL, nur für POCSAG-Locationversand: Aus dem Modul [Geocoding](../modul/geocoding.md): 63 | 64 | - `lat` 65 | - `lon` 66 | 67 | --- 68 | ## Externe Abhängigkeiten 69 | keine 70 | -------------------------------------------------------------------------------- /docu/docs/information/broadcast.md: -------------------------------------------------------------------------------- 1 | #
Broadcast Service
2 | 3 | Durch den Broadcast Service haben Clients die Möglichkeit, automatisch den Server zu finden und sich mit diesem zu verbinden. Dazu stellt der Server die benötigten Verbinungsinformationen per Broadcast Service bereit. 4 | 5 | **Hinweis:** *Server und Client müssen sich im selben Subnetz befinden.* 6 | 7 | --- 8 | ## Aufbau 9 | 10 | Der Broadcast Service besteht aus 2 Teilen - einem Server und einem Clienten. 11 | Nachfolgend soll der Ablauf einer Verbindung des Clienten zum Server mittels des Broadcast Services erklärt werden. 12 | 13 | ![Broadcast](../img/broadcast.png){ .center } 14 | 15 | --- 16 | ## Ablauf 17 | 18 | ### Schritt 1 - Broadcast Server starten 19 | Im ersten Schritt wird auf dem Server ein zusätzlicher Broadcast Server in einem seperaten Thread gestartet. Dieser lauscht auf einem festgelegten Port auf UDP Broadcast Pakete. Nun kann eine beliebige Anzahl von Clienten mittels des Broadcast Services die Verbindungsgdaten des Servers abfragen. 20 | 21 | ### Schritt 2 - Broadcast durch Clienten 22 | Die Client Applikation startet nun zur Abfrage der Verbindungsdaten einen BC Clienten und sendet dort auf dem festgelegten Port ein Paket per UDP Boradcast. Der Inhalt des Paketes ist das Magic-Word `` und wird von allen im selben Subnetz befindlichen Gegenstellen empfangen. Nun wartet der Client auf eine Antwort des Broadcast Server mit den Verbindungsdaten. 23 | 24 | ### Schritt 3 - Verbindungsdaten senden 25 | Wird nun ein Broadcast Paket empfangen, prüft der BC Server die Daten auf das Magic-Word ``. Wird dieses erkannt, liest der Server die Absender-IP-Addresse aus dem Paket aus und sendet eine Antwort direkt an diesen Clienten. Dieses Antwortpaket sieht folgendermaßen aus: `;8080` wobei die `8080` hier den normalen TCP Kommunikationsport des Servers darstellt. 26 | 27 | ### Schritt 4 - Verbindungsdaten empfangen 28 | Nachdem der Client das direkt an ihn gerichtete Paket mit den Verbindungsdaten vom Server empfangen hat, prüft er auf das Magic-Word ``. Ist dieses enthalten wird der Port für die TCP Verbindung aus dem Paket extrahiert. Außerdem wird die IP-Addresse des Absenders aus dem Paket gelesen. 29 | Anschließend stehen dem Clienten die Verbindungsdaten des Servers zur Verfügung und er kann sich per TCP auf den angegebenen Port mit dem BOSWatch Server verbinden um seine Alarmierungs-Pakete abzusetzen. 30 | 31 | Da der Broadcast Server in einem eigenen Thread, unabhängig vom Hauptprogram läuft, können ganz einfach weitere Clienten per Broadcast Service die Verbindungsdaten des Servers abrufen. 32 | -------------------------------------------------------------------------------- /module/filter/regexFilter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: regexFilter.py 13 | @date: 26.10.2019 14 | @author: Bastian Schroll 15 | @description: Regex filter module 16 | """ 17 | import logging 18 | from module.moduleBase import ModuleBase 19 | 20 | # ###################### # 21 | # Custom plugin includes # 22 | import re 23 | # ###################### # 24 | 25 | logging.debug("- %s loaded", __name__) 26 | 27 | 28 | class BoswatchModule(ModuleBase): 29 | r"""!Regex based filter mechanism""" 30 | def __init__(self, config): 31 | r"""!Do not change anything here!""" 32 | super().__init__(__name__, config) # you can access the config class on 'self.config' 33 | 34 | def onLoad(self): 35 | r"""!Called by import of the plugin""" 36 | pass 37 | 38 | def doWork(self, bwPacket): 39 | r"""!start an run of the module. 40 | 41 | @param bwPacket: A BOSWatch packet instance""" 42 | for regexFilter in self.config: 43 | checkFailed = False 44 | logging.debug("try filter '%s' with %d check(s)", regexFilter.get("name"), len(regexFilter.get("checks"))) 45 | 46 | for check in regexFilter.get("checks"): 47 | fieldData = bwPacket.get(check.get("field")) 48 | 49 | if not fieldData or not re.search(check.get("regex"), fieldData): 50 | logging.debug("[-] field '%s' with regex '%s'", check.get("field"), check.get("regex")) 51 | checkFailed = True 52 | break # if one check failed we break this filter 53 | else: 54 | logging.debug("[+] field '%s' with regex '%s'", check.get("field"), check.get("regex")) 55 | 56 | if not checkFailed: 57 | logging.debug("[PASSED] filter '%s'", regexFilter.get("name")) 58 | return None # None -> Router will go on with this packet 59 | logging.debug("[FAILED] filter '%s'", regexFilter.get("name")) 60 | 61 | return False # False -> Router will stop further processing 62 | 63 | def onUnload(self): 64 | r"""!Called by destruction of the plugin""" 65 | pass 66 | -------------------------------------------------------------------------------- /test/boswatch/test_broadcast.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: test_broadcast.py 13 | @date: 25.09.2018 14 | @author: Bastian Schroll 15 | @description: Unittests for BOSWatch. File have to run as "pytest" unittest 16 | """ 17 | # problem of the pytest fixtures 18 | # pylint: disable=redefined-outer-name 19 | import logging 20 | import pytest 21 | 22 | from boswatch.network.broadcast import BroadcastServer 23 | from boswatch.network.broadcast import BroadcastClient 24 | 25 | 26 | def setup_function(function): 27 | logging.debug("[TEST] %s.%s", function.__module__, function.__name__) 28 | 29 | 30 | @pytest.fixture() 31 | def broadcastServer(): 32 | r"""!Server a BroadcastServer instance""" 33 | broadcastServer = BroadcastServer() 34 | yield broadcastServer 35 | if broadcastServer.isRunning: 36 | assert broadcastServer.stop() 37 | while broadcastServer.isRunning: 38 | pass 39 | 40 | 41 | @pytest.fixture() 42 | def broadcastClient(): 43 | r"""!Server a BroadcastClient instance""" 44 | return BroadcastClient() 45 | 46 | 47 | def test_serverStartStop(broadcastServer): 48 | r"""!Start a BroadcastServer, check if running and stop it""" 49 | assert broadcastServer.start() 50 | assert broadcastServer.isRunning 51 | assert broadcastServer.stop() 52 | 53 | 54 | def test_serverDoubleStart(broadcastServer): 55 | r"""!Try to start a BroadcastServer twice""" 56 | assert broadcastServer.start() 57 | assert broadcastServer.start() 58 | assert broadcastServer.stop() 59 | 60 | 61 | def test_serverStopNotStarted(broadcastServer): 62 | r"""!Try to stop a BroadcastServer where is not running""" 63 | assert broadcastServer.stop() 64 | 65 | 66 | def test_clientWithoutServer(broadcastClient): 67 | r"""!Use BroadcastClient with no server""" 68 | assert not broadcastClient.getConnInfo(1) 69 | 70 | 71 | def test_serverClientFetchConnInfo(broadcastClient, broadcastServer): 72 | r"""!Fetch connection info from BroadcastServer""" 73 | assert broadcastServer.start() 74 | assert broadcastClient.getConnInfo() 75 | assert broadcastServer.stop() 76 | assert broadcastClient.serverIP 77 | assert broadcastClient.serverPort 78 | -------------------------------------------------------------------------------- /test/module/test_descriptor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: test_descriptor.py 13 | @date: 14.04.2020 14 | @author: Bastian Schroll 15 | @description: Unittests for BOSWatch. File have to run as "pytest" unittest 16 | """ 17 | # problem of the pytest fixtures 18 | # pylint: disable=redefined-outer-name 19 | import logging 20 | import pytest 21 | from boswatch.utils import paths 22 | 23 | from boswatch.configYaml import ConfigYAML 24 | from boswatch.packet import Packet 25 | from module.descriptor import BoswatchModule as Descriptor 26 | 27 | 28 | def setup_method(method): 29 | logging.debug("[TEST] %s.%s", method.__module__, method.__name__) 30 | 31 | 32 | @pytest.fixture 33 | def makeDescriptor(): 34 | r"""!Build a descriptor object with loaded configuration""" 35 | config = ConfigYAML() 36 | assert config.loadConfigFile(paths.TEST_PATH + "test_config.yaml") is True 37 | descriptor = Descriptor(config.get("descriptor_test")) 38 | return descriptor 39 | 40 | 41 | @pytest.fixture 42 | def makePacket(): 43 | r"""!Build a BW Packet object""" 44 | packet = Packet() 45 | return packet 46 | 47 | 48 | def test_descriptorFoundFirst(makeDescriptor, makePacket): 49 | r"""!Run descriptor on the first entry in list""" 50 | makePacket.set("tone", "12345") 51 | makePacket = makeDescriptor.doWork(makePacket) 52 | assert makePacket.get("description") == "Test 12345" 53 | 54 | 55 | def test_descriptorFoundSecond(makeDescriptor, makePacket): 56 | r"""!Run descriptor on the second entry in list""" 57 | makePacket.set("tone", "23456") 58 | makePacket = makeDescriptor.doWork(makePacket) 59 | assert makePacket.get("description") == "Test 23456" 60 | 61 | 62 | def test_descriptorNotFound(makeDescriptor, makePacket): 63 | r"""!Run descriptor no matching data found""" 64 | makePacket.set("tone", "99999") 65 | makePacket = makeDescriptor.doWork(makePacket) 66 | assert makePacket.get("description") == "99999" 67 | 68 | 69 | def test_descriptorScanFieldNotAvailable(makeDescriptor, makePacket): 70 | r"""!Run descriptor on a non existent scanField""" 71 | makePacket = makeDescriptor.doWork(makePacket) 72 | assert makePacket.get("description") is None 73 | -------------------------------------------------------------------------------- /boswatch/utils/header.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: header.py 13 | @date: 11.12.2017 14 | @author: Bastian Schroll 15 | @description: Prints the BOSWatch Header on Screen or logfile 16 | """ 17 | import logging 18 | import platform # for python version nr 19 | 20 | from boswatch.utils import version 21 | 22 | logging.debug("- %s loaded", __name__) 23 | 24 | 25 | def logoToLog(): 26 | r"""!Prints the BOSWatch logo to the log at debug level 27 | 28 | @return True or False on error""" 29 | logging.debug(r" ____ ____ ______ __ __ __ _____ ") 30 | logging.debug(r" / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / ") 31 | logging.debug(r" / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < ") 32 | logging.debug(r" / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / ") 33 | logging.debug(r"/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ ") 34 | logging.debug(r" German BOS Information Script ") 35 | logging.debug(r" by Bastian Schroll ") 36 | logging.debug(r"") 37 | return True 38 | 39 | 40 | def infoToLog(): 41 | r"""!Prints the BOSWatch and OS information to log at debug level 42 | 43 | @return True or False on error""" 44 | logging.debug("BOSWatch and environment information") 45 | logging.debug("- Client version: %d.%d.%d", 46 | version.client["major"], 47 | version.client["minor"], 48 | version.client["patch"]) 49 | logging.debug("- Server version: %d.%d.%d", 50 | version.server["major"], 51 | version.server["minor"], 52 | version.server["patch"]) 53 | logging.debug("- Branch: %s", 54 | version.branch) 55 | logging.debug("- Release date: %02d.%02d.%4d", 56 | version.date["day"], 57 | version.date["month"], 58 | version.date["year"]) 59 | logging.debug("- Python version: %s", platform.python_version()) 60 | logging.debug("- Python build: %s", platform.python_build()) 61 | logging.debug("- System: %s", platform.system()) 62 | logging.debug("- OS Version: %s", platform.platform()) 63 | logging.debug("") 64 | return True 65 | -------------------------------------------------------------------------------- /boswatch/configYaml.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: configYaml.py 13 | @date: 27.02.2019 14 | @author: Bastian Schroll 15 | @description: Module for the configuration in YAML format 16 | """ 17 | import logging 18 | import yaml 19 | import yaml.parser 20 | 21 | logging.debug("- %s loaded", __name__) 22 | 23 | 24 | class ConfigYAML: 25 | 26 | def __init__(self, config=None): 27 | self._config = config 28 | 29 | def __iter__(self): 30 | for item in self._config: 31 | if type(item) is list or type(item) is dict: 32 | yield ConfigYAML(item) 33 | else: 34 | yield item 35 | 36 | def __len__(self): 37 | r"""!returns the length of an config element""" 38 | return len(self._config) 39 | 40 | def __str__(self): 41 | r"""!Returns the string representation of the internal config dict""" 42 | return str(self._config) 43 | 44 | def loadConfigFile(self, configPath): 45 | r"""!loads a given configuration file 46 | 47 | @param configPath: Path to the config file 48 | @return True or False""" 49 | logging.debug("load config file from: %s", configPath) 50 | try: 51 | with open(configPath) as file: 52 | # use safe_load instead load 53 | self._config = yaml.safe_load(file) 54 | return True 55 | except FileNotFoundError: 56 | logging.error("config file not found: %s", configPath) 57 | except yaml.parser.ParserError: 58 | logging.exception("syntax error in config file: %s", configPath) 59 | return False 60 | 61 | def get(self, *args, default=None): 62 | r"""!Get a single value from the config 63 | or a value set in a new configYAML class instance 64 | 65 | @param *args: Config section (one ore more strings) 66 | @param default: Default value if section not found (None) 67 | @return: A single value, a value set in an configYAML instance, the default value""" 68 | tmp = self._config 69 | try: 70 | for arg in args: 71 | tmp = tmp.get(arg, default) 72 | if type(tmp) is list or type(tmp) is dict: 73 | return ConfigYAML(tmp) 74 | else: 75 | return tmp 76 | except AttributeError: # pragma: no cover 77 | return default 78 | -------------------------------------------------------------------------------- /boswatch/inputSource/lineInInput.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: lienInInput.py 13 | @date: 18.04.2020 14 | @author: Philipp von Kirschbaum 15 | @description: Input source for line-in with alsa 16 | """ 17 | import logging 18 | from boswatch.utils import paths 19 | from boswatch.processManager import ProcessManager 20 | from boswatch.inputSource.inputBase import InputBase 21 | 22 | logging.debug("- %s loaded", __name__) 23 | 24 | 25 | class LineInInput(InputBase): 26 | r"""!Class for the line-in input source""" 27 | 28 | def _runThread(self, dataQueue, lineInConfig, decoderConfig): 29 | lineInProc = None 30 | mmProc = None 31 | try: 32 | lineInProc = ProcessManager("arecord") 33 | lineInProc.addArgument("-q ") # supress any other outputs 34 | lineInProc.addArgument("-f S16_LE") # set output format (16bit) 35 | lineInProc.addArgument("-r 22050") # set output sampling rate (22050Hz) 36 | lineInProc.addArgument("-D plughw:" + 37 | str(lineInConfig.get("card", default="1")) + 38 | "," + 39 | str(lineInConfig.get("device", default="0"))) # device id 40 | lineInProc.setStderr(open(paths.LOG_PATH + "asla.log", "a")) 41 | lineInProc.start() 42 | 43 | mmProc = self.getDecoderInstance(decoderConfig, lineInProc.stdout) 44 | mmProc.start() 45 | 46 | logging.info("start decoding") 47 | while self._isRunning: 48 | if not lineInProc.isRunning: 49 | logging.warning("asla was down - try to restart") 50 | lineInProc.start() 51 | 52 | if lineInProc.isRunning: 53 | logging.info("rtl_fm is back up - restarting multimon...") 54 | mmProc.setStdin(lineInProc.stdout) 55 | mmProc.start() 56 | elif not mmProc.isRunning: 57 | logging.warning("multimon was down - try to restart") 58 | mmProc.start() 59 | elif lineInProc.isRunning and mmProc.isRunning: 60 | line = mmProc.readline() 61 | if line: 62 | self.addToQueue(line) 63 | except: 64 | logging.exception("error in lineIn input routine") 65 | finally: 66 | mmProc.stop() 67 | lineInProc.stop() 68 | -------------------------------------------------------------------------------- /boswatch/router/router.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: router.py 13 | @date: 01.03.2019 14 | @author: Bastian Schroll 15 | @description: Class for the BOSWatch packet router 16 | """ 17 | import logging 18 | import copy 19 | import time 20 | 21 | logging.debug("- %s loaded", __name__) 22 | 23 | 24 | class Router: 25 | r"""!Class for the Router""" 26 | def __init__(self, name): 27 | r"""!Create a new router 28 | 29 | @param name: name of the router""" 30 | self.name = name 31 | self.routeList = [] 32 | 33 | # for time counting 34 | self._cumTime = 0 35 | self._routerTime = 0 36 | 37 | # for statistics 38 | self._runCount = 0 39 | 40 | logging.debug("[%s] add new router", self.name) 41 | 42 | def addRoute(self, route): 43 | r"""!Adds a route point to the router 44 | 45 | @param route: instance of the Route class 46 | """ 47 | logging.debug("[%s] add route: %s", self.name, route.name) 48 | self.routeList.append(route) 49 | 50 | def runRouter(self, bwPacket): 51 | r"""!Run the router 52 | 53 | @param bwPacket: instance of Packet class 54 | @return a instance of Packet class 55 | """ 56 | self._runCount += 1 57 | tmpTime = time.time() 58 | 59 | logging.debug("[%s] started", self.name) 60 | 61 | for routeObject in self.routeList: 62 | logging.debug("[%s] -> run route: %s", self.name, routeObject.name) 63 | bwPacket_tmp = routeObject.callback(copy.deepcopy(bwPacket)) # copy bwPacket to prevent edit the original 64 | 65 | if bwPacket_tmp is None: # returning None doesnt change the bwPacket 66 | continue 67 | 68 | if bwPacket_tmp is False: # returning False stops the router immediately 69 | logging.debug("[%s] stopped", self.name) 70 | break 71 | 72 | bwPacket = bwPacket_tmp 73 | logging.debug("[%s] bwPacket returned", self.name) 74 | logging.debug("[%s] finished", self.name) 75 | 76 | self._routerTime = time.time() - tmpTime 77 | self._cumTime += self._routerTime 78 | 79 | return bwPacket 80 | 81 | def _getStatistics(self): 82 | r"""!Returns statistical information's from last router run 83 | 84 | @return Statistics as pyton dict""" 85 | stats = {"type": "router", 86 | "runCount": self._runCount, 87 | "cumTime": self._cumTime, 88 | "moduleTime": self._routerTime} 89 | return stats 90 | -------------------------------------------------------------------------------- /boswatch/inputSource/pulseaudioInput.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: pulseaudioInput.py 13 | @date: 18.04.2020, 29.06.2020 14 | @author: Philipp von Kirschbaum, b-watch 15 | @description: Input source for PulseAudio 16 | """ 17 | import logging 18 | from boswatch.utils import paths 19 | from boswatch.processManager import ProcessManager 20 | from boswatch.inputSource.inputBase import InputBase 21 | 22 | logging.debug("- %s loaded", __name__) 23 | 24 | 25 | class PulseAudioInput(InputBase): 26 | r"""!Class for the PulseAudio input source""" 27 | 28 | def _runThread(self, dataQueue, PulseAudioConfig, decoderConfig): 29 | PulseAudioProc = None 30 | mmProc = None 31 | try: 32 | PulseAudioProc = ProcessManager("parec") 33 | PulseAudioProc.addArgument("--channels=1") # supress any other outputs 34 | PulseAudioProc.addArgument("--format=s16le") # set output format (16bit) 35 | PulseAudioProc.addArgument("--rate=22050") # set output sampling rate (22050Hz) 36 | PulseAudioProc.addArgument("--device=" + 37 | str(PulseAudioConfig.get("device", default="boswatch")) + 38 | ".monitor") # sink name 39 | PulseAudioProc.setStderr(open(paths.LOG_PATH + "pulseaudio.log", "a")) 40 | PulseAudioProc.start() 41 | 42 | mmProc = self.getDecoderInstance(decoderConfig, PulseAudioProc.stdout) 43 | mmProc.start() 44 | 45 | logging.info("start decoding") 46 | while self._isRunning: 47 | if not PulseAudioProc.isRunning: 48 | logging.warning("PulseAudio was down - try to restart") 49 | PulseAudioProc.start() 50 | 51 | if PulseAudioProc.isRunning: 52 | logging.info("rtl_fm is back up - restarting multimon...") 53 | mmProc.setStdin(PulseAudioProc.stdout) 54 | mmProc.start() 55 | elif not mmProc.isRunning: 56 | logging.warning("multimon was down - try to restart") 57 | mmProc.start() 58 | elif PulseAudioProc.isRunning and mmProc.isRunning: 59 | line = mmProc.readline() 60 | if line: 61 | self.addToQueue(line) 62 | except: 63 | logging.exception("error in PulseAudio input routine") 64 | finally: 65 | mmProc.stop() 66 | PulseAudioProc.stop() 67 | -------------------------------------------------------------------------------- /module/geocoding.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: geocoding.py 13 | @date: 22.02.2020 14 | @author: Jan Speller 15 | @description: Geocoding Module 16 | """ 17 | import logging 18 | from module.moduleBase import ModuleBase 19 | 20 | # ###################### # 21 | # Custom plugin includes # 22 | import geocoder 23 | import re 24 | # ###################### # 25 | 26 | logging.debug("- %s loaded", __name__) 27 | 28 | 29 | class BoswatchModule(ModuleBase): 30 | r"""!Description of the Module""" 31 | def __init__(self, config): 32 | r"""!Do not change anything here!""" 33 | super().__init__(__name__, config) # you can access the config class on 'self.config' 34 | 35 | def doWork(self, bwPacket): 36 | r"""!start an run of the module. 37 | 38 | @param bwPacket: A BOSWatch packet instance""" 39 | if bwPacket.get("mode") == "pocsag": 40 | self.geocode(bwPacket) 41 | 42 | return bwPacket 43 | 44 | def geocode(self, bwPacket): 45 | r"""!find address in message and get latitude and longitude 46 | 47 | @param bwPacket: A BOSWatch packet instance""" 48 | try: 49 | addressArray = re.search(self.config.get("regex"), bwPacket.get("message")) 50 | provider = self.config.get("apiProvider") 51 | 52 | if addressArray[1] is None: 53 | logging.info("No address found, skipping geocoding") 54 | return bwPacket 55 | 56 | address = addressArray[1] 57 | bwPacket.set("address", address) 58 | self.registerWildcard("{ADDRESS}", "address") 59 | logging.info("Found address: '" + address + "' in packet") 60 | 61 | if "mapbox" == provider: 62 | logging.info("Using Mapbox as provider") 63 | g = geocoder.mapbox(address, key=self.config.get("apiToken")) 64 | elif "google" == provider: 65 | logging.info("Using Google as provider") 66 | g = geocoder.google(address, key=self.config.get("apiToken")) 67 | else: 68 | return bwPacket 69 | 70 | (lat, lon) = g.latlng 71 | logging.info("Found following coordinates for address: [lat=" + str(lat) + ", lon=" + str(lon) + "]") 72 | bwPacket.set("lat", lat) 73 | bwPacket.set("lon", lon) 74 | self.registerWildcard("{LAT}", "lat") 75 | self.registerWildcard("{LON}", "lon") 76 | 77 | return bwPacket 78 | except Exception as e: 79 | logging.exception("Unknown Error while executing geocoding module: " + str(type(e).__name__) + ": " + str(e)) 80 | return bwPacket 81 | -------------------------------------------------------------------------------- /boswatch/decoder/pocsagDecoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: pocsagDecoder.py 13 | @date: 15.10.2025 14 | @author: Bastian Schroll 15 | @description: Decoder class for pocsag 16 | """ 17 | 18 | import logging 19 | import re 20 | 21 | from boswatch.packet import Packet 22 | 23 | logging.debug("- %s loaded", __name__) 24 | 25 | 26 | class PocsagDecoder: 27 | r"""!POCSAG decoder class 28 | 29 | This class decodes POCSAG data. 30 | First step is to validate the data and _check if the format is correct. 31 | In the last step a valid BOSWatch packet is created and returned""" 32 | 33 | @staticmethod 34 | def decode(data): 35 | r"""!Decodes POCSAG 36 | 37 | @param data: POCSAG for decoding 38 | @return BOSWatch POCSAG packet or None""" 39 | bitrate, ric, subric = PocsagDecoder._getBitrateRicSubric(data) 40 | 41 | # If no valid SubRIC (Function 0–3) detected → abort 42 | if subric is None: 43 | logging.warning("Invalid POCSAG function (not 0–3)") 44 | return None 45 | 46 | if ric and len(ric) == 7: 47 | if "Alpha:" in data: 48 | message = data.split('Alpha:')[1].strip() 49 | message = re.sub(r'<\s*(?:NUL|EOT)\s*>?', '', message).strip() 50 | else: 51 | message = "" 52 | subricText = subric.replace("1", "a").replace("2", "b").replace("3", "c").replace("4", "d") 53 | 54 | logging.debug("found valid POCSAG") 55 | 56 | bwPacket = Packet() 57 | bwPacket.set("mode", "pocsag") 58 | bwPacket.set("bitrate", bitrate) 59 | bwPacket.set("ric", ric) 60 | bwPacket.set("subric", subric) 61 | bwPacket.set("subricText", subricText) 62 | bwPacket.set("message", message) 63 | 64 | return bwPacket 65 | 66 | logging.warning("no valid POCSAG") 67 | return None 68 | 69 | @staticmethod 70 | def _getBitrateRicSubric(data): 71 | """Gets the Bitrate, Ric and Subric from data using robust regex parsing.""" 72 | bitrate = "0" 73 | ric = None 74 | subric = None 75 | 76 | # determine bitrate 77 | if "POCSAG512:" in data: 78 | bitrate = "512" 79 | elif "POCSAG1200:" in data: 80 | bitrate = "1200" 81 | elif "POCSAG2400:" in data: 82 | bitrate = "2400" 83 | 84 | # extract RIC (address) 85 | m_ric = re.search(r'Address:\s*(\d{1,7})(?=\s|$)', data) 86 | if m_ric: 87 | ric = m_ric.group(1).zfill(7) 88 | 89 | # extract SubRIC (function) 90 | m_sub = re.search(r'Function:\s*([0-3])', data) 91 | if m_sub: 92 | subric = str(int(m_sub.group(1)) + 1) 93 | 94 | return bitrate, ric, subric 95 | -------------------------------------------------------------------------------- /docu/docs/information/router.md: -------------------------------------------------------------------------------- 1 | #
Routing Mechanismus
2 | 3 | BOSWatch 3 hat einen Routing Mechanismus integriert. Mit diesem ist es auf einfache Weise möglich, den Verlauf von Alarmpaketen zu steuern. 4 | 5 | 6 | --- 7 | ## Ablauf 8 | 9 | Nachfolgender Ablauf soll am Beispiel eines Alarms mit einem Pocsag Paket erklärt werden. 10 | 11 | ![Routing-Mechanismus](../img/router.png){ .center } 12 | 13 | - BOSWatch startet alle Router, welche in der config als `alarmRouter` konfiguriert worden sind (in diesem Fall nur `Router1`) 14 | - Der Router `Router1` beginnt seine Ausführung und arbeitet die einzelnen Routenpunkte sequentiell ab 15 | - Das Modul `descriptor` wird aufgerufen und fügt ggf. Beschreibungen zum Paket hinzu 16 | - Das Modul `doubleFilter` wird aufgerufen und blockiert doppelte Alarme 17 | (hier würde die Ausführung dieses Routers und damit des kompletten Alarmprozesses stoppen wenn der Alarm als doppelter erkannt würde) 18 | - Der Router `Router2` wir nun aufgerufen (bis zur Rückkehr aus `Router2` ist der Router `Router1` angehalten) 19 | - Der Router `Router2` beginnt seine Ausführung und arbeitet die einzelnen Routenpunkte sequentiell ab 20 | - Das Modul `modeFilter` wird aufgerufen und stoppt den Router da es sich nicht um ein FMS Paket handelt 21 | - Es wird zur Ausführung von `Router1` zurückgekehrt 22 | - Der Router `Router3` beginnt seine Ausführung und arbeitet die einzelnen Routenpunkte sequentiell ab 23 | - Das Modul `modeFilter` wird aufgerufen und leitet das Paket weiter da es sich um ein Pocsag Paket handelt 24 | - Das Plugin `Telegram` wird aufgerufen 25 | - Das Plugin `MySQL` wird augerufen 26 | - Es wird zur Ausführung von `Router1` zurückgekehrt 27 | - Der Router `Router1` setzt seine Ausführung fort 28 | - Das Modul `modeFilter` wird aufgerufen und stoppt den Router da es sich nicht um ein ZVEI Paket handelt 29 | 30 | Jetzt sind alle Routenpunkte abgearbeitet und die Alarmierung damit abgeschlossen. 31 | 32 | --- 33 | ## Konfiguration 34 | 35 | Nachfolgend ist die Router Konfiguration des BW3-Servers für das obige Beispiel zu finden: 36 | 37 | ```yaml 38 | alarmRouter: 39 | - Router1 40 | 41 | router: 42 | - name: Router1 43 | route: 44 | - type: module 45 | res: descriptor 46 | config: 47 | [...] 48 | - type: module 49 | res: filter.doubleFilter 50 | config: 51 | [...] 52 | - type: router 53 | res: Router2 54 | - type: router 55 | res: Router3 56 | - type: module 57 | res: filter.modeFilter 58 | config: 59 | allowed: 60 | - zvei 61 | - type: plugin 62 | res: sms 63 | config: 64 | [...] 65 | 66 | - name: Router2 67 | route: 68 | - type: module 69 | res: filter.modeFilter 70 | config: 71 | allowed: 72 | - fms 73 | - type: plugin 74 | res: mysql 75 | config: 76 | [...] 77 | 78 | - name: Router3 79 | route: 80 | - type: module 81 | res: filter.modeFilter 82 | config: 83 | allowed: 84 | - pocsag 85 | - type: plugin 86 | res: telegram 87 | config: 88 | [...] 89 | - type: plugin 90 | res: mysql 91 | config: 92 | [...] 93 | ``` 94 | -------------------------------------------------------------------------------- /plugin/http.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: http.py 13 | @date: 23.02.2020 14 | @author: Jan Speller 15 | @description: Http Plugin 16 | """ 17 | import logging 18 | from plugin.pluginBase import PluginBase 19 | 20 | # ###################### # 21 | # Custom plugin includes # 22 | import asyncio 23 | from aiohttp import ClientSession 24 | # ###################### # 25 | 26 | logging.debug("- %s loaded", __name__) 27 | 28 | 29 | class BoswatchPlugin(PluginBase): 30 | r"""!Description of the Plugin""" 31 | def __init__(self, config): 32 | r"""!Do not change anything here!""" 33 | super().__init__(__name__, config) # you can access the config class on 'self.config' 34 | 35 | def fms(self, bwPacket): 36 | r"""!Called on FMS alarm 37 | 38 | @param bwPacket: bwPacket instance 39 | Remove if not implemented""" 40 | urls = self.config.get("fms") 41 | self._makeRequests(urls) 42 | 43 | def pocsag(self, bwPacket): 44 | r"""!Called on POCSAG alarm 45 | 46 | @param bwPacket: bwPacket instance 47 | Remove if not implemented""" 48 | urls = self.config.get("pocsag") 49 | self._makeRequests(urls) 50 | 51 | def zvei(self, bwPacket): 52 | r"""!Called on ZVEI alarm 53 | 54 | @param bwPacket: bwPacket instance 55 | Remove if not implemented""" 56 | urls = self.config.get("zvei") 57 | self._makeRequests(urls) 58 | 59 | def msg(self, bwPacket): 60 | r"""!Called on MSG packet 61 | 62 | @param bwPacket: bwPacket instance 63 | Remove if not implemented""" 64 | urls = self.config.get("msg") 65 | self._makeRequests(urls) 66 | 67 | def _makeRequests(self, urls): 68 | """Parses wildcard urls and handles asynchronus requests 69 | 70 | @param urls: array of urls""" 71 | urls = [self.parseWildcards(url) for url in urls] 72 | 73 | loop = asyncio.get_event_loop() 74 | 75 | future = asyncio.ensure_future(self._asyncRequests(urls)) 76 | loop.run_until_complete(future) 77 | 78 | async def _asyncRequests(self, urls): 79 | """Handles asynchronus requests 80 | 81 | @param urls: array of urls to send requests to""" 82 | tasks = [] 83 | 84 | async with ClientSession() as session: 85 | for url in urls: 86 | task = asyncio.ensure_future(self._fetch(url, session)) 87 | tasks.append(task) 88 | 89 | responses = asyncio.gather(*tasks) 90 | await responses 91 | 92 | async def _fetch(self, url, session): 93 | """Fetches requests 94 | 95 | @param url: url 96 | 97 | @param session: Clientsession instance""" 98 | async with session.get(url) as response: 99 | logging.info("{} returned [{}]".format(response.url, response.status)) 100 | return await response.read() 101 | -------------------------------------------------------------------------------- /boswatch/inputSource/sdrInput.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: sdrInput.py 13 | @date: 28.10.2018 14 | @author: Bastian Schroll 15 | @description: Input source for sdr with rtl_fm 16 | """ 17 | import logging 18 | import time 19 | from boswatch.utils import paths 20 | from boswatch.processManager import ProcessManager 21 | from boswatch.inputSource.inputBase import InputBase 22 | 23 | logging.debug("- %s loaded", __name__) 24 | 25 | 26 | class SdrInput(InputBase): 27 | r"""!Class for the sdr input source""" 28 | 29 | def _runThread(self, dataQueue, sdrConfig, decoderConfig): 30 | sdrProc = None 31 | mmProc = None 32 | try: 33 | sdrProc = ProcessManager(str(sdrConfig.get("rtlPath", default="rtl_fm"))) 34 | sdrProc.addArgument("-d " + str(sdrConfig.get("device", default="0"))) # device id 35 | sdrProc.addArgument("-f " + str(sdrConfig.get("frequency"))) # frequencies 36 | sdrProc.addArgument("-p " + str(sdrConfig.get("error", default="0"))) # frequency error in ppm 37 | sdrProc.addArgument("-l " + str(sdrConfig.get("squelch", default="1"))) # squelch 38 | sdrProc.addArgument("-g " + str(sdrConfig.get("gain", default="100"))) # gain 39 | if (sdrConfig.get("fir_size", default=None) is not None): 40 | sdrProc.addArgument("-F " + str(sdrConfig.get("fir_size"))) # fir_size 41 | sdrProc.addArgument("-M fm") # set mode to fm 42 | sdrProc.addArgument("-E DC") # set DC filter 43 | sdrProc.addArgument("-s 22050") # bit rate of audio stream 44 | sdrProc.setStderr(open(paths.LOG_PATH + "rtl_fm.log", "a")) 45 | sdrProc.start() 46 | 47 | mmProc = self.getDecoderInstance(decoderConfig, sdrProc.stdout) 48 | mmProc.start() 49 | 50 | logging.info("start decoding") 51 | while self._isRunning: 52 | if not sdrProc.isRunning: 53 | logging.warning("rtl_fm was down - trying to restart in 10 seconds") 54 | time.sleep(10) 55 | 56 | sdrProc.start() 57 | if sdrProc.isRunning: 58 | logging.info("rtl_fm is back up - restarting multimon...") 59 | mmProc.setStdin(sdrProc.stdout) 60 | mmProc.start() 61 | elif not mmProc.isRunning: 62 | logging.warning("multimon was down - try to restart") 63 | mmProc.start() 64 | elif sdrProc.isRunning and mmProc.isRunning: 65 | line = mmProc.readline() 66 | if line: 67 | self.addToQueue(line) 68 | except: 69 | logging.exception("error in sdr input routine") 70 | finally: 71 | mmProc.stop() 72 | sdrProc.stop() 73 | -------------------------------------------------------------------------------- /test/boswatch/test_timer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: test_timer.py 13 | @date: 21.09.2018 14 | @author: Bastian Schroll 15 | @description: Unittests for BOSWatch. File have to run as "pytest" unittest 16 | """ 17 | # problem of the pytest fixtures 18 | # pylint: disable=redefined-outer-name 19 | import logging 20 | import time 21 | import pytest 22 | 23 | from boswatch.timer import RepeatedTimer 24 | 25 | 26 | def setup_function(function): 27 | logging.debug("[TEST] %s.%s", function.__module__, function.__name__) 28 | 29 | 30 | def testTargetFast(): 31 | r"""!Fast worker thread""" 32 | logging.debug("run testTargetFast") 33 | 34 | 35 | def testTargetSlow(): 36 | r"""!Slow worker thread""" 37 | logging.debug("run testTargetSlow start") 38 | time.sleep(0.51) 39 | logging.debug("run testTargetSlow end") 40 | 41 | 42 | @pytest.fixture() 43 | def useTimerFast(): 44 | r"""!Server a RepeatedTimer instance with fast worker""" 45 | testTimer = RepeatedTimer(0.1, testTargetFast) 46 | yield testTimer 47 | if testTimer.isRunning: 48 | assert testTimer.stop() 49 | 50 | 51 | @pytest.fixture() 52 | def useTimerSlow(): 53 | r"""!Server a RepeatedTimer instance slow worker""" 54 | testTimer = RepeatedTimer(0.1, testTargetSlow) 55 | yield testTimer 56 | if testTimer.isRunning: 57 | assert testTimer.stop() 58 | 59 | 60 | def test_timerStartStop(useTimerFast): 61 | r"""!Try to start and stop a timer""" 62 | assert useTimerFast.start() 63 | assert useTimerFast.stop() 64 | 65 | 66 | def test_timerDoubleStart(useTimerFast): 67 | r"""!Try to start a timer twice""" 68 | assert useTimerFast.start() 69 | assert useTimerFast.start() 70 | assert useTimerFast.stop() 71 | 72 | 73 | def test_timerStopNotStarted(useTimerFast): 74 | r"""!Try to stop a timer where is not started""" 75 | assert useTimerFast.stop() 76 | 77 | 78 | def test_timerIsRunning(useTimerFast): 79 | r"""!Check if a timer is running""" 80 | assert useTimerFast.start() 81 | assert useTimerFast.isRunning 82 | assert useTimerFast.stop() 83 | 84 | 85 | def test_timerRun(useTimerFast): 86 | r"""!Run a timer and check overdue and lostEvents""" 87 | assert useTimerFast.start() 88 | time.sleep(0.2) 89 | assert useTimerFast.stop() 90 | assert useTimerFast.overdueCount == 0 91 | assert useTimerFast.lostEvents == 0 92 | 93 | 94 | def test_timerOverdue(useTimerSlow): 95 | r"""!Run a timer and check overdue and lostEvents""" 96 | assert useTimerSlow.start() 97 | time.sleep(0.2) 98 | assert useTimerSlow.stop() 99 | assert useTimerSlow.overdueCount == 1 100 | assert useTimerSlow.lostEvents == 5 101 | 102 | 103 | def test_timerOverdueLong(useTimerSlow): 104 | r"""!Run a timer and check overdue and lostEvents""" 105 | assert useTimerSlow.start() 106 | time.sleep(1) 107 | assert useTimerSlow.stop() 108 | assert useTimerSlow.overdueCount == 2 109 | assert useTimerSlow.lostEvents == 10 110 | -------------------------------------------------------------------------------- /boswatch/inputSource/inputBase.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: inoutSource.py 13 | @date: 28.10.2018 14 | @author: Bastian Schroll 15 | @description: Base class for boswatch input sources 16 | """ 17 | import time 18 | import logging 19 | import threading 20 | from abc import ABC, abstractmethod 21 | from boswatch.utils import paths 22 | from boswatch.processManager import ProcessManager 23 | from boswatch.decoder.decoder import Decoder 24 | 25 | logging.debug("- %s loaded", __name__) 26 | 27 | 28 | class InputBase(ABC): 29 | r"""!Base class for handling inout sources""" 30 | 31 | def __init__(self, inputQueue, inputConfig, decoderConfig): 32 | r"""!Build a new InputSource class 33 | 34 | @param inputQueue: Python queue object to store input data 35 | @param inputConfig: ConfigYaml object with the inoutSource config 36 | @param decoderConfig: ConfigYaml object with the decoder config""" 37 | self._inputThread = None 38 | self._isRunning = False 39 | self._inputQueue = inputQueue 40 | self._inputConfig = inputConfig 41 | self._decoderConfig = decoderConfig 42 | 43 | def start(self): 44 | r"""!Start the input source thread""" 45 | logging.debug("starting input thread") 46 | self._isRunning = True 47 | self._inputThread = threading.Thread(target=self._runThread, name="inputThread", 48 | args=(self._inputQueue, self._inputConfig, self._decoderConfig)) 49 | self._inputThread.daemon = True 50 | self._inputThread.start() 51 | 52 | @abstractmethod 53 | def _runThread(self, dataQueue, sdrConfig, decoderConfig): 54 | r"""!Thread routine of the input source has to be inherit""" 55 | 56 | def shutdown(self): 57 | r"""!Stop the input source thread""" 58 | if self._isRunning: 59 | logging.debug("wait for stopping the input thread") 60 | self._isRunning = False 61 | self._inputThread.join() 62 | logging.debug("input thread stopped") 63 | 64 | def addToQueue(self, data): 65 | r"""!Decode and add alarm data to the queue for further processing during boswatch client""" 66 | bwPacket = Decoder.decode(data) 67 | if bwPacket is not None: 68 | self._inputQueue.put_nowait((bwPacket, time.time())) 69 | logging.debug("Added received data to queue") 70 | 71 | def getDecoderInstance(self, decoderConfig, StdIn): 72 | mmProc = ProcessManager(str(decoderConfig.get("path", default="multimon-ng")), textMode=True) 73 | if decoderConfig.get("fms", default=0): 74 | mmProc.addArgument("-a FMSFSK") 75 | if decoderConfig.get("zvei", default=0): 76 | mmProc.addArgument("-a ZVEI1") 77 | if decoderConfig.get("poc512", default=0): 78 | mmProc.addArgument("-a POCSAG512") 79 | if decoderConfig.get("poc1200", default=0): 80 | mmProc.addArgument("-a POCSAG1200") 81 | if decoderConfig.get("poc2400", default=0): 82 | mmProc.addArgument("-a POCSAG2400") 83 | if decoderConfig.get("char", default=0): 84 | mmProc.addArgument("-C " + str(decoderConfig.get("char"))) 85 | mmProc.addArgument("-f alpha") 86 | mmProc.addArgument("-t raw -") 87 | mmProc.setStdin(StdIn) 88 | mmProc.setStderr(open(paths.LOG_PATH + "multimon-ng.log", "a")) 89 | return mmProc 90 | -------------------------------------------------------------------------------- /module/filter/doubleFilter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: doubleFilter.py 13 | @date: 09.07.2020 14 | @author: Bastian Schroll, b-watch 15 | @description: Filter module for double packages 16 | """ 17 | import logging 18 | from module.moduleBase import ModuleBase 19 | 20 | # ###################### # 21 | # Custom plugin includes # 22 | import time 23 | # ###################### 24 | 25 | logging.debug("- %s loaded", __name__) 26 | 27 | 28 | class BoswatchModule(ModuleBase): 29 | r"""!Description of the Module""" 30 | def __init__(self, config): 31 | r"""!Do not change anything here!""" 32 | super().__init__(__name__, config) # you can access the config class on 'self.config' 33 | self._filterLists = {} 34 | logging.debug("Configured ignoreTime: %d", self.config.get("ignoreTime", default=10)) 35 | logging.debug("Configured maxEntry: %d", self.config.get("maxEntry", default=10)) 36 | 37 | def onLoad(self): 38 | r"""!Called by import of the plugin 39 | Remove if not implemented""" 40 | pass 41 | 42 | def doWork(self, bwPacket): 43 | r"""!start an run of the module. 44 | 45 | @param bwPacket: A BOSWatch packet instance""" 46 | if bwPacket.get("mode") == "fms": 47 | filterFields = ["fms"] 48 | elif bwPacket.get("mode") == "pocsag": 49 | filterFields = self.config.get("pocsagFields", default=["ric", "subric"]) 50 | elif bwPacket.get("mode") == "zvei": 51 | filterFields = ["tone"] 52 | else: 53 | logging.error("No Filter for '%s'", bwPacket) 54 | return False 55 | 56 | if not bwPacket.get("mode") in self._filterLists: 57 | logging.debug("create new doubleFilter list for '%s'", bwPacket.get("mode")) 58 | self._filterLists[bwPacket.get("mode")] = [] 59 | 60 | logging.debug("filterFields for '%s' is '%s'", bwPacket.get("mode"), ", ".join(filterFields)) 61 | 62 | return self._check(bwPacket, filterFields) 63 | 64 | def onUnload(self): 65 | r"""!Called by destruction of the plugin 66 | Remove if not implemented""" 67 | pass 68 | 69 | def _check(self, bwPacket, filterFields): 70 | self._filterLists[bwPacket.get("mode")].insert(0, bwPacket) 71 | 72 | for listPacket in self._filterLists[bwPacket.get("mode")][1:]: # [1:] skip first entry, thats the new one 73 | if all(listPacket.get(x) == bwPacket.get(x) for x in filterFields): 74 | logging.debug("found duplicate: %s", bwPacket.get("mode")) 75 | return False 76 | # delete entries that are to old 77 | counter = 0 78 | for listPacket in self._filterLists[bwPacket.get("mode")][1:]: # [1:] skip first entry, thats the new one 79 | if float(listPacket.get("timestamp")) < (time.time() - self.config.get("ignoreTime", default=10)): 80 | self._filterLists[bwPacket.get("mode")].remove(listPacket) 81 | counter += 1 82 | if counter: 83 | logging.debug("%d old entry(s) removed", counter) 84 | 85 | # delete last entry if list is to big 86 | if len(self._filterLists[bwPacket.get("mode")]) > self.config.get("maxEntry", default=20): 87 | logging.debug("MaxEntry reached - delete oldest") 88 | self._filterLists[bwPacket.get("mode")].pop() 89 | 90 | logging.debug("doubleFilter ok") 91 | return None 92 | -------------------------------------------------------------------------------- /test/boswatch/test_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: test_config.py 13 | @date: 08.01.2017 14 | @author: Bastian Schroll 15 | @description: Unittests for BOSWatch. File have to run as "pytest" unittest 16 | """ 17 | # problem of the pytest fixtures 18 | # pylint: disable=redefined-outer-name 19 | import logging 20 | import pytest 21 | from boswatch.utils import paths 22 | 23 | from boswatch.configYaml import ConfigYAML 24 | 25 | 26 | def setup_function(function): 27 | logging.debug("[TEST] %s.%s", function.__module__, function.__name__) 28 | 29 | 30 | @pytest.fixture 31 | def getConfig(): 32 | r"""!Build a config object""" 33 | return ConfigYAML() 34 | 35 | 36 | @pytest.fixture 37 | def getFilledConfig(): 38 | r"""!Build a config object and fill it with the config data""" 39 | filledConfig = ConfigYAML() 40 | assert filledConfig.loadConfigFile(paths.TEST_PATH + "test_config.yaml") is True 41 | return filledConfig 42 | 43 | 44 | def test_loadConfigFile(getConfig): 45 | r"""!load a config file""" 46 | assert getConfig.loadConfigFile(paths.TEST_PATH + "test_config.yaml") is True 47 | 48 | 49 | def test_loadConfigFileFailed(getConfig): 50 | r"""!load a config file with syntax error""" 51 | assert getConfig.loadConfigFile(paths.TEST_PATH + "test_configFailed.yaml") is False 52 | 53 | 54 | def test_loadConfigFileNotFound(getConfig): 55 | r"""!load a config file where is not available""" 56 | assert getConfig.loadConfigFile(paths.TEST_PATH + "test_configNotFound.yaml") is False 57 | 58 | 59 | def test_getConfigAsString(getFilledConfig): 60 | r"""!Get the string representation of the config""" 61 | assert type(str(getFilledConfig)) is str 62 | logging.debug(getFilledConfig) 63 | 64 | 65 | def test_getTypes(getFilledConfig): 66 | r"""!Get and check different data types in config""" 67 | assert type(getFilledConfig.get("types")) is ConfigYAML 68 | assert type(getFilledConfig.get("types", "string")) is str 69 | assert type(getFilledConfig.get("types", "bool")) is bool 70 | assert type(getFilledConfig.get("types", "integer")) is int 71 | assert type(getFilledConfig.get("types", "float")) is float 72 | 73 | 74 | def test_getDefaultValue(getFilledConfig): 75 | r"""!Get the default value of an not existent entry""" 76 | assert getFilledConfig.get("notExistent", default="defaultValue") == "defaultValue" 77 | 78 | 79 | def test_getNestedConfig(getFilledConfig): 80 | r"""!Work with nested sub-config elements""" 81 | nestedConfig = getFilledConfig.get("types") 82 | assert type(nestedConfig) is ConfigYAML 83 | assert nestedConfig.get("string") == "Hello World" 84 | 85 | 86 | def test_configIterationList(getFilledConfig): 87 | r"""!Try to iterate over a list in the config""" 88 | counter = 0 89 | for item in getFilledConfig.get("list"): 90 | assert type(item) is str 91 | counter += 1 92 | assert counter == 3 93 | 94 | 95 | def test_configIterationListWithNestedList(getFilledConfig): 96 | r"""!Try to iterate over a list in the config where its elements are lists itself""" 97 | listCnt = 0 98 | strCnt = 0 99 | for item in getFilledConfig.get("list1"): 100 | if type(item) is ConfigYAML: 101 | listCnt += 1 102 | if type(item) is str: 103 | strCnt += 1 104 | assert listCnt == 2 105 | assert strCnt == 1 106 | -------------------------------------------------------------------------------- /module/moduleBase.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: moduleBase.py 13 | @date: 01.03.2019 14 | @author: Bastian Schroll 15 | @description: Module main class to inherit 16 | """ 17 | import logging 18 | import time 19 | from abc import ABC 20 | 21 | from boswatch import wildcard 22 | 23 | logging.debug("- %s loaded", __name__) 24 | 25 | 26 | class ModuleBase(ABC): 27 | r"""!Main module class""" 28 | 29 | _modulesActive = [] 30 | 31 | def __init__(self, moduleName, config): 32 | r"""!init preload some needed locals and then call onLoad() directly""" 33 | self._moduleName = moduleName 34 | self.config = config 35 | self._modulesActive.append(self) 36 | 37 | # for time counting 38 | self._cumTime = 0 39 | self._moduleTime = 0 40 | 41 | # for statistics 42 | self._runCount = 0 43 | self._moduleErrorCount = 0 44 | 45 | logging.debug("[%s] onLoad()", moduleName) 46 | self.onLoad() 47 | 48 | def _cleanup(self): 49 | r"""!Cleanup routine calls onUnload() directly""" 50 | logging.debug("[%s] onUnload()", self._moduleName) 51 | self._modulesActive.remove(self) 52 | self.onUnload() 53 | 54 | def _run(self, bwPacket): 55 | r"""!start an run of the module. 56 | 57 | @param bwPacket: A BOSWatch packet instance 58 | @return bwPacket or False""" 59 | self._runCount += 1 60 | logging.debug("[%s] run #%d", self._moduleName, self._runCount) 61 | 62 | tmpTime = time.time() 63 | try: 64 | logging.debug("[%s] doWork()", self._moduleName) 65 | bwPacket = self.doWork(bwPacket) 66 | except: 67 | self._moduleErrorCount += 1 68 | logging.exception("[%s] alarm error", self._moduleName) 69 | self._moduleTime = time.time() - tmpTime 70 | 71 | self._cumTime += self._moduleTime 72 | 73 | logging.debug("[%s] took %0.3f seconds", self._moduleName, self._moduleTime) 74 | 75 | return bwPacket 76 | 77 | def _getStatistics(self): 78 | r"""!Returns statistical information's from last module run 79 | 80 | @return Statistics as pyton dict""" 81 | stats = {"type": "module", 82 | "runCount": self._runCount, 83 | "cumTime": self._cumTime, 84 | "moduleTime": self._moduleTime, 85 | "moduleErrorCount": self._moduleErrorCount} 86 | return stats 87 | 88 | def onLoad(self): 89 | r"""!Called by import of the module 90 | can be inherited""" 91 | pass 92 | 93 | def doWork(self, bwPacket): 94 | r"""!Called module run 95 | can be inherited 96 | 97 | @param bwPacket: bwPacket instance""" 98 | logging.warning("no functionality in module %s", self._moduleName) 99 | 100 | def onUnload(self): 101 | r"""!Called on shutdown of boswatch 102 | can be inherited""" 103 | pass 104 | 105 | @staticmethod 106 | def registerWildcard(newWildcard, bwPacketField): 107 | r"""!Register a new wildcard 108 | 109 | @param newWildcard: wildcard where parser searching for 110 | @param bwPacketField: field from bwPacket where holds replacement data""" 111 | if not newWildcard.startswith("{") or not newWildcard.endswith("}"): 112 | logging.error("wildcard not registered - false format: %s", newWildcard) 113 | return 114 | if bwPacketField == "": 115 | logging.error("wildcard not registered - bwPacket field is empty") 116 | return 117 | wildcard.registerWildcard(newWildcard, bwPacketField) 118 | -------------------------------------------------------------------------------- /boswatch/timer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: timer.py 13 | @date: 21.09.2018 14 | @author: Bastian Schroll 15 | @description: Timer class for interval timed events 16 | """ 17 | import logging 18 | import time 19 | from threading import Thread, Event 20 | 21 | logging.debug("- %s loaded", __name__) 22 | 23 | 24 | class RepeatedTimer: 25 | 26 | def __init__(self, interval, targetFunction, *args, **kwargs): 27 | r"""!Create a new instance of the RepeatedTimer 28 | 29 | @param interval: interval in sec. to recall target function 30 | @param targetFunction: function to call on timer event 31 | @param *args: arguments for the called function 32 | @param *kwargs: keyword arguments for the called function 33 | """ 34 | self._interval = interval 35 | self._function = targetFunction 36 | self._args = args 37 | self._kwargs = kwargs 38 | self._start = 0 39 | self.overdueCount = 0 40 | self.lostEvents = 0 41 | self._isRunning = False 42 | self._event = Event() 43 | self._thread = None 44 | 45 | def start(self): 46 | r"""!Start a new timer worker thread 47 | 48 | @return True or False""" 49 | if self._thread is None: 50 | self._event.clear() 51 | self._thread = Thread(target=self._target) 52 | self._thread.name = "RepTim(" + str(self._interval) + ")" 53 | self._thread.daemon = True # start as daemon (thread dies if main program ends) 54 | self._thread.start() 55 | logging.debug("start repeatedTimer: %s", self._thread.name) 56 | return True 57 | logging.debug("repeatedTimer always started") 58 | return True 59 | 60 | def stop(self): 61 | r"""!Stop the timer worker thread 62 | 63 | @return True or False""" 64 | if self._thread is not None: 65 | logging.debug("stop repeatedTimer: %s", self._thread.name) 66 | self._event.set() 67 | if self._thread is not None: 68 | self._thread.join() 69 | return True 70 | logging.warning("repeatedTimer always stopped") 71 | return True 72 | 73 | def _target(self): 74 | r"""!Runs the target function with his arguments in own thread""" 75 | self._start = time.time() 76 | while not self._event.wait(self.restTime): 77 | logging.debug("work") 78 | startTime = time.time() 79 | 80 | try: 81 | self._function(*self._args, **self._kwargs) 82 | except: # pragma: no cover 83 | logging.exception("target throws an exception") 84 | 85 | runTime = time.time() - startTime 86 | if runTime < self._interval: 87 | logging.debug("ready after: %0.3f sec. - next call in: %0.3f sec.", runTime, self.restTime) 88 | else: 89 | lostEvents = int(runTime / self._interval) 90 | logging.warning("timer overdue! interval: %0.3f sec. - runtime: %0.3f sec. - " 91 | "%d events lost - next call in: %0.3f sec.", self._interval, runTime, lostEvents, self.restTime) 92 | self.lostEvents += lostEvents 93 | self.overdueCount += 1 94 | logging.debug("repeatedTimer thread stopped: %s", self._thread.name) 95 | self._thread = None # set to none after leave teh thread (running recognize) 96 | 97 | @property 98 | def isRunning(self): 99 | r"""!Property for repeatedTimer running state""" 100 | if self._thread: 101 | return True 102 | return False 103 | 104 | @property 105 | def restTime(self): 106 | r"""!Property to get remaining time till next call""" 107 | return self._interval - ((time.time() - self._start) % self._interval) 108 | -------------------------------------------------------------------------------- /boswatch/network/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: client.py 13 | @date: 09.12.2017 14 | @author: Bastian Schroll 15 | @description: Class implementation for a TCP socket client 16 | """ 17 | import logging 18 | import socket 19 | import select 20 | 21 | logging.debug("- %s loaded", __name__) 22 | 23 | HEADERSIZE = 10 24 | 25 | 26 | class TCPClient: 27 | r"""!TCP client class""" 28 | 29 | def __init__(self, timeout=3): 30 | r"""!Create a new instance 31 | 32 | @param timeout: timeout for the client in sec. (3)""" 33 | socket.setdefaulttimeout(timeout) 34 | self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 35 | 36 | def connect(self, host="localhost", port=8080): 37 | r"""!Connect to the server 38 | 39 | @param host: Server IP address ("localhost") 40 | @param port: Server Port (8080) 41 | @return True or False""" 42 | try: 43 | if not self.isConnected: 44 | self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 45 | self._sock.connect((host, port)) 46 | logging.debug("connected to %s:%s", host, port) 47 | return True 48 | logging.warning("client always connected") 49 | return True 50 | except socket.error as e: 51 | logging.error(e) 52 | return False 53 | 54 | def disconnect(self): 55 | r"""!Disconnect from the server 56 | 57 | @return True or False""" 58 | try: 59 | if self.isConnected: 60 | self._sock.shutdown(socket.SHUT_RDWR) 61 | self._sock.close() 62 | logging.debug("disconnected") 63 | return True 64 | logging.warning("client always disconnected") 65 | return True 66 | except socket.error as e: 67 | logging.error(e) 68 | return False 69 | 70 | def transmit(self, data): 71 | r"""!Send a data packet to the server 72 | 73 | @param data: data to send to the server 74 | @return True or False""" 75 | try: 76 | logging.debug("transmitting:\n%s", data) 77 | data = data.encode("utf-8") 78 | header = str(len(data)).ljust(HEADERSIZE).encode("utf-8") 79 | self._sock.sendall(header + data) 80 | logging.debug("transmitted...") 81 | return True 82 | except socket.error as e: 83 | logging.error(e) 84 | return False 85 | 86 | def receive(self, timeout=1): 87 | r"""!Receive data from the server 88 | 89 | @param timeout: to wait for incoming data in seconds 90 | @return received data""" 91 | try: 92 | read, _, _ = select.select([self._sock], [], [], timeout) 93 | if not read: # check if there is something to read 94 | return False 95 | 96 | header = self._sock.recv(HEADERSIZE).decode("utf-8") 97 | if not len(header): # check if there data 98 | return False 99 | 100 | length = int(header.strip()) 101 | received = self._sock.recv(length).decode("utf-8") 102 | 103 | logging.debug("recv header: '%s'", header) 104 | logging.debug("received %d bytes: %s", len(received), received) 105 | return received 106 | except socket.error as e: 107 | logging.error(e) 108 | return False 109 | 110 | @property 111 | def isConnected(self): 112 | r"""!Property of client connected state""" 113 | try: 114 | if self._sock: 115 | _, write, _ = select.select([], [self._sock], [], 0.1) 116 | if write: 117 | data = "".encode("utf-8") 118 | header = str(len(data)).ljust(HEADERSIZE).encode("utf-8") 119 | self._sock.sendall(header + data) 120 | return True 121 | return False 122 | except socket.error as e: 123 | if e.errno != 32: 124 | logging.exception(e) 125 | return False 126 | except ValueError: 127 | return False 128 | -------------------------------------------------------------------------------- /bw_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: bw_server.py 13 | @date: 09.12.2017 14 | @author: Bastian Schroll 15 | @description: BOSWatch server application 16 | """ 17 | # pylint: disable=wrong-import-position 18 | # pylint: disable=wrong-import-order 19 | 20 | import sys 21 | major_version = sys.version_info.major 22 | assert major_version >= 3, "please use python3 to run BosWatch 3" 23 | 24 | from boswatch.utils import paths 25 | 26 | if not paths.makeDirIfNotExist(paths.LOG_PATH): 27 | print("cannot find/create log directory: %s", paths.LOG_PATH) 28 | exit(1) 29 | 30 | import logging.config 31 | logging.config.fileConfig(paths.CONFIG_PATH + "logger_server.ini") 32 | logging.debug("") 33 | logging.debug("######################## NEW LOG ############################") 34 | logging.debug("BOSWatch server has started ...") 35 | 36 | 37 | logging.debug("Import python modules") 38 | import argparse 39 | logging.debug("- argparse") 40 | import queue 41 | logging.debug("- queue") 42 | import time 43 | logging.debug("- time") 44 | 45 | logging.debug("Import BOSWatch modules") 46 | from boswatch.configYaml import ConfigYAML 47 | from boswatch.network.server import TCPServer 48 | from boswatch.packet import Packet 49 | from boswatch.utils import header 50 | from boswatch.network.broadcast import BroadcastServer 51 | from boswatch.router.routerManager import RouterManager 52 | from boswatch.utils import misc 53 | 54 | header.logoToLog() 55 | header.infoToLog() 56 | 57 | # With -h or --help you get the Args help 58 | parser = argparse.ArgumentParser(prog="bw_server.py", 59 | description="""BOSWatch is a Python Script to receive and 60 | decode german BOS information with rtl_fm and multimon-NG""", 61 | epilog="""More options you can find in the extern client.ini 62 | file in the folder /config""") 63 | parser.add_argument("-c", "--config", help="Name to configuration File", required=True) 64 | args = parser.parse_args() 65 | 66 | 67 | bwConfig = ConfigYAML() 68 | if not bwConfig.loadConfigFile(paths.CONFIG_PATH + args.config): 69 | logging.fatal("cannot load config file") 70 | exit(1) 71 | 72 | # ############################# begin server system 73 | bwRoutMan = None 74 | bwServer = None 75 | bcServer = None 76 | 77 | try: 78 | bwRoutMan = RouterManager() 79 | if not bwRoutMan.buildRouters(bwConfig): 80 | logging.fatal("Error while building routers") 81 | exit(1) 82 | 83 | bcServer = BroadcastServer() 84 | if bwConfig.get("server", "useBroadcast", default=False): 85 | bcServer.start() 86 | 87 | incomingQueue = queue.Queue() 88 | bwServer = TCPServer(incomingQueue) 89 | if bwServer.start(port=bwConfig.get('server', 'port', default=8080)): 90 | 91 | while 1: 92 | if incomingQueue.empty(): # pause only when no data 93 | time.sleep(0.1) # reduce cpu load (wait 100ms) 94 | # in worst case a packet have to wait 100ms until it will be processed 95 | 96 | else: 97 | data = incomingQueue.get() 98 | 99 | logging.info("get data from %s (waited in queue %0.3f sec.)", data[0], time.time() - data[2]) 100 | logging.debug("%s packet(s) still waiting in queue", incomingQueue.qsize()) 101 | bwPacket = Packet((data[1])) 102 | 103 | bwPacket.set("clientIP", data[0]) 104 | misc.addServerDataToPacket(bwPacket, bwConfig) 105 | 106 | logging.debug("[ --- ALARM --- ]") 107 | bwRoutMan.runRouters(bwConfig.get("alarmRouter"), bwPacket) 108 | logging.debug("[ --- END ALARM --- ]") 109 | 110 | incomingQueue.task_done() 111 | 112 | except KeyboardInterrupt: # pragma: no cover 113 | logging.warning("Keyboard interrupt") 114 | except SystemExit: # pragma: no cover 115 | logging.error("BOSWatch interrupted by an error") 116 | except: # pragma: no cover 117 | logging.exception("BOSWatch interrupted by an error") 118 | finally: 119 | logging.debug("Starting shutdown routine") 120 | if bwRoutMan: 121 | bwRoutMan.cleanup() 122 | if bwServer: 123 | bwServer.stop() 124 | if bcServer: 125 | bcServer.stop() 126 | logging.debug("BOSWatch server has stopped ...") 127 | -------------------------------------------------------------------------------- /boswatch/wildcard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: wildcard.py 13 | @date: 23.07.2025 14 | @author: Bastian Schroll 15 | @description: Functions to replace wildcards in stings 16 | """ 17 | import logging 18 | import time 19 | 20 | logging.debug("- %s loaded", __name__) 21 | 22 | _additionalWildcards = {} 23 | 24 | 25 | def registerWildcard(wildcard, bwPacketField): 26 | r"""!Register a new additional wildcard 27 | 28 | @param wildcard: New wildcard string with format: '{WILDCARD}' 29 | @param bwPacketField: Field of the bwPacket which is used for wildcard replacement""" 30 | if wildcard in _additionalWildcards: 31 | logging.error("wildcard always registered: %s", wildcard) 32 | return 33 | logging.debug("register new wildcard %s for field: %s", wildcard, bwPacketField) 34 | _additionalWildcards[wildcard] = bwPacketField 35 | 36 | 37 | def replaceWildcards(message, bwPacket): 38 | r"""!Replace the wildcards in a given message 39 | 40 | @param message: Message in which wildcards should be replaced 41 | @param bwPacket: bwPacket instance with the replacement information 42 | @return Input message with the replaced wildcards""" 43 | 44 | # Start with wildcards that are always available 45 | _wildcards = { 46 | # formatting wildcards 47 | # todo check if br and par are needed - if not also change config 48 | "{BR}": "\r\n", 49 | "{LPAR}": "(", 50 | "{RPAR}": ")", 51 | "{TIME}": time.strftime("%d.%m.%Y %H:%M:%S"), 52 | 53 | # info wildcards 54 | # server 55 | "{SNAME}": bwPacket.get("serverName"), 56 | "{SVERS}": bwPacket.get("serverVersion"), 57 | "{SDATE}": bwPacket.get("serverBuildDate"), 58 | "{SBRCH}": bwPacket.get("serverBranch"), 59 | 60 | # client 61 | "{CNAME}": bwPacket.get("clientName"), 62 | "{CIP}": bwPacket.get("clientIP"), 63 | "{CVERS}": bwPacket.get("clientVersion"), 64 | "{CDATE}": bwPacket.get("clientBuildDate"), 65 | "{CBRCH}": bwPacket.get("clientBranch"), 66 | 67 | # boswatch wildcards 68 | "{INSRC}": bwPacket.get("inputSource"), 69 | "{TIMES}": bwPacket.get("timestamp"), 70 | "{FREQ}": bwPacket.get("frequency"), 71 | "{MODE}": bwPacket.get("mode"), 72 | } 73 | 74 | # Get the packet mode to add specific wildcards 75 | mode = bwPacket.get("mode") 76 | 77 | # fms wildcards 78 | if mode == "fms": 79 | fms_wildcards = { 80 | "{FMS}": bwPacket.get("fms"), 81 | "{SERV}": bwPacket.get("service"), 82 | "{COUNT}": bwPacket.get("country"), 83 | "{LOC}": bwPacket.get("location"), 84 | "{VEHC}": bwPacket.get("vehicle"), 85 | "{STAT}": bwPacket.get("status"), 86 | "{DIR}": bwPacket.get("direction"), 87 | "{DIRT}": bwPacket.get("directionText"), 88 | "{TACI}": bwPacket.get("tacticalInfo"), 89 | } 90 | _wildcards.update(fms_wildcards) 91 | 92 | # pocsag wildcards 93 | elif mode == "pocsag": 94 | pocsag_wildcards = { 95 | "{BIT}": bwPacket.get("bitrate"), 96 | "{RIC}": bwPacket.get("ric"), 97 | "{SRIC}": bwPacket.get("subric"), 98 | "{SRICT}": bwPacket.get("subricText"), 99 | "{MSG}": bwPacket.get("message"), 100 | } 101 | _wildcards.update(pocsag_wildcards) 102 | 103 | # zvei wildcards 104 | elif mode == "zvei": 105 | zvei_wildcards = { 106 | "{TONE}": bwPacket.get("tone"), 107 | } 108 | _wildcards.update(zvei_wildcards) 109 | 110 | # msg wildcards 111 | elif mode == "msg": 112 | msg_wildcards = { 113 | "{MSG}": bwPacket.get("message"), 114 | } 115 | _wildcards.update(msg_wildcards) 116 | 117 | # Now, replace all collected wildcards 118 | for wildcard, field in _wildcards.items(): 119 | # Only replace if the field was found in the packet (is not None) 120 | if field is not None: 121 | message = message.replace(wildcard, field) 122 | 123 | # Handle additional, dynamically registered wildcards 124 | for wildcard, fieldName in _additionalWildcards.items(): 125 | field = bwPacket.get(fieldName) 126 | if field is not None: 127 | message = message.replace(wildcard, field) 128 | 129 | return message 130 | -------------------------------------------------------------------------------- /docu/docs/modul/descriptor.md: -------------------------------------------------------------------------------- 1 | #
Descriptor
2 | --- 3 | 4 | ## Beschreibung 5 | Mit diesem Modul können einem Alarmpaket beliebige Beschreibungen in Abhängigkeit der enthaltenen Informationen hinzugefügt werden. 6 | 7 | ## Unterstützte Alarmtypen 8 | - Fms 9 | - Pocsag 10 | - Zvei 11 | - Msg 12 | 13 | ## Resource 14 | `descriptor` 15 | 16 | ## Konfiguration 17 | Informationen zum Aufbau eines [BOSWatch Pakets](../develop/packet.md) 18 | 19 | **Achtung:** Zahlen, die führende Nullen enthalten, müssen in der YAML-Konfiguration in Anführungszeichen gesetzt werden, z.B. `'0012345'`. In CSV-Dateien ist dies nicht zwingend erforderlich. 20 | 21 | |Feld|Beschreibung|Default| 22 | |----|------------|-------| 23 | |scanField|Feld des BW Pakets, welches geprüft werden soll|| 24 | |descrField|Name des Feldes im BW Paket, in welchem die Beschreibung gespeichert werden soll|| 25 | |wildcard|Optional: Es kann für das angelegte `descrField` automatisch ein Wildcard registriert werden|None| 26 | |descriptions|Liste der Beschreibungen|| 27 | |csvPath|Pfad der CSV-Datei (relativ zum Projektverzeichnis)|| 28 | 29 | #### `descriptions:` 30 | |Feld|Beschreibung|Default| 31 | |----|------------|-------| 32 | |for|Wert im `scanField`, der geprüft werden soll|| 33 | |add|Beschreibungstext, der im `descrField` hinterlegt werden soll|| 34 | |isRegex|Muss explizit auf `true` gesetzt werden, wenn RegEx verwendet wird|false| 35 | 36 | **Beispiel:** 37 | ```yaml 38 | - type: module 39 | res: descriptor 40 | config: 41 | - scanField: tone 42 | descrField: description 43 | wildcard: "{DESCR}" 44 | descriptions: 45 | - for: 12345 46 | add: FF DescriptorTest 47 | - for: '05678' # führende Nullen in '' ! 48 | add: FF TestDescription 49 | - for: '890(1[1-9]|2[0-9])' # Regex-Pattern in '' ! 50 | add: Feuerwehr Wache \\1 (BF) 51 | isRegex: true 52 | - scanField: status 53 | descrField: fmsStatDescr 54 | wildcard: "{STATUSTEXT}" 55 | descriptions: 56 | - for: 1 57 | add: Frei (Funk) 58 | - for: 2 59 | add: Frei (Wache) 60 | - ... 61 | ``` 62 | 63 | **Wichtige Punkte für YAML-Regex:** 64 | - Apostroph: Regex-Pattern sollten in `'` stehen, um YAML-Parsing-Probleme zu vermeiden 65 | - isRegex-Flag: Muss explizit auf `true` gesetzt werden 66 | - Escaping: Backslashes müssen in YAML doppelt escaped werden (`\\1` statt `\1`) 67 | - Regex-Gruppen: Mit `\\1`, `\\2` etc. können Teile des Matches in der Beschreibung verwendet werden 68 | 69 | #### `csvPath:` 70 | 71 | **Beispiel:** 72 | ``` 73 | - type: module 74 | res: descriptor 75 | config: 76 | - scanField: tone 77 | descrField: description 78 | wildcard: "{DESCR}" 79 | csvPath: "config/descriptions_tone.csv" 80 | ``` 81 | 82 | `csvPath` gibt den Pfad zu einer CSV-Datei an, relativ zum Projektverzeichnis (z. B. `"config/descriptions_tone.csv"`). 83 | 84 | Eine neue CSV-Datei (z. B. `descriptions_tone.csv`) hat folgendes Format: 85 | 86 | **Beispiel** 87 | ``` 88 | for,add,isRegex 89 | 11111,KBI Landkreis Z,false 90 | 12345,FF A-Dorf,false 91 | 23456,FF B-Dorf 92 | ^3456[0-9]$,FF Grossdorf,true 93 | ``` 94 | 95 | **Hinweis:** In CSV-Dateien müssen Werte mit führenden Nullen **nicht** in Anführungszeichen gesetzt werden (können aber, falls gewünscht). Die Spalte `isRegex` gibt an, ob der Wert in `for` als regulärer Ausdruck interpretiert werden soll (true/false). Falls kein Wert angegeben ist, ist die Paarung standardmäßig `false`, wie z.B. beim Eintrag der FF B-Dorf im obigen Beispiel. 96 | 97 | ### Kombination von YAML- und CSV-Konfiguration 98 | 99 | Beide Varianten können parallel genutzt werden. In diesem Fall werden die Beschreibungen aus der YAML-Konfiguration und aus der angegebenen CSV-Datei in einer gemeinsamen Datenbank zusammengeführt. 100 | 101 | #### Matching-Reihenfolge und Priorität 102 | 103 | Das Modul wendet folgende Prioritäten beim Matching an: 104 | 105 | 1. **Exakte Matches** (aus YAML und CSV) werden zuerst geprüft 106 | 2. **Regex-Matches** (aus YAML und CSV) werden nur geprüft, wenn kein exakter Match gefunden wurde 107 | 108 | **Beispiel für Kombination:** 109 | ```yaml 110 | - type: module 111 | res: descriptor 112 | config: 113 | - scanField: tone 114 | descrField: description 115 | wildcard: "{DESCR}" 116 | descriptions: 117 | - for: 12345 118 | add: FF YAML-Test (exakt) 119 | - for: '123.*' 120 | add: FF YAML-Regex 121 | isRegex: true 122 | csvPath: "config/descriptions_tone.csv" 123 | ``` 124 | 125 | Bei einem `scanField`-Wert von `12345` wird **immer** "FF YAML-Test (exakt)" verwendet, auch wenn der Regex ebenfalls zutreffen würde. Regex-Matches werden nur verwendet, wenn kein exakter Match gefunden wurde - unabhängig davon, ob die Einträge aus YAML oder CSV stammen. 126 | 127 | --- 128 | ## Modul Abhängigkeiten 129 | - keine 130 | 131 | --- 132 | ## Externe Abhängigkeiten 133 | - keine 134 | 135 | --- 136 | ## Paket Modifikationen 137 | - Wenn im Paket das Feld `scanField` vorhanden ist, wird das Feld `descrField` dem Paket hinzugefügt 138 | - Wenn keine Beschreibung vorhanden ist, wird im Feld `descrField` der Inhalt des Feldes `scanField` hinterlegt 139 | 140 | --- 141 | ## Zusätzliche Wildcards 142 | - Von der Konfiguration abhängig -------------------------------------------------------------------------------- /boswatch/processManager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: processManager.py 13 | @date: 04.03.2018 14 | @author: Bastian Schroll 15 | @description: Class for managing sub processes 16 | """ 17 | import logging 18 | import subprocess 19 | 20 | logging.debug("- %s loaded", __name__) 21 | 22 | 23 | class ProcessManager: 24 | r"""!class to manage a extern sub process""" 25 | def __init__(self, process, textMode=False): 26 | logging.debug("create process instance %s - textMode: %s", process, textMode) 27 | self._args = [] 28 | self._args.append(process) 29 | self._stdin = None 30 | self._stdout = subprocess.PIPE 31 | self._stderr = subprocess.STDOUT 32 | self._processHandle = None 33 | self._textMode = textMode 34 | 35 | def addArgument(self, arg): 36 | r"""!add a new argument 37 | 38 | @param arg: argument to add as string""" 39 | logging.debug("add argument to process: %s -> %s", self._args[0], arg) 40 | for splitArg in arg.split(): 41 | self._args.append(splitArg) 42 | 43 | def clearArguments(self): 44 | r"""!clear all arguments""" 45 | self._args = self._args[0:1] # kept first element (process name) 46 | 47 | def start(self): 48 | r"""!start the new process 49 | 50 | @return: True or False""" 51 | logging.debug("start new process: %s %s", self._args[0], self._args[1:]) 52 | try: 53 | self._processHandle = subprocess.Popen(self._args, 54 | stdin=self._stdin, 55 | stdout=self._stdout, 56 | stderr=self._stderr, 57 | universal_newlines=self._textMode, 58 | shell=False) 59 | if not self.isRunning: 60 | logging.error("cannot start process") 61 | return False 62 | logging.debug("process started with PID %d", self._processHandle.pid) 63 | return True 64 | 65 | except FileNotFoundError: 66 | logging.error("File not found: %s", self._args[0]) 67 | return False 68 | 69 | def stop(self): 70 | r"""!Stop the process by sending SIGTERM and wait for ending""" 71 | logging.debug("stopping process: %s", self._args[0]) 72 | if self.isRunning: 73 | self._processHandle.terminate() 74 | while self.isRunning: 75 | pass 76 | logging.debug("process %s returned %d", self._args[0], self._processHandle.returncode) 77 | 78 | def readline(self): 79 | r"""!Read one line from stdout stream 80 | 81 | @return singe line or None""" 82 | if self.isRunning and self._stdout is not None: 83 | try: 84 | line = self._processHandle.stdout.readline().strip() 85 | except UnicodeDecodeError: 86 | return None 87 | return line 88 | return None 89 | 90 | def skipLines(self, lineCount=1): 91 | r"""!Skip given number of lines from the output 92 | 93 | @param lineCount: number of lines to skip 94 | """ 95 | logging.debug("skip %d lines from output", lineCount) 96 | while self.isRunning and lineCount: 97 | self.readline() 98 | lineCount -= 1 99 | 100 | def skipLinesUntil(self, matchText): 101 | r"""!Skip lines from the output until the given string is in it 102 | 103 | @param matchText: string to search for in output 104 | """ 105 | logging.debug("skip lines till '%s' from output", matchText) 106 | if not self._textMode: 107 | matchText = bytes(matchText, "utf-8") 108 | while self.isRunning and matchText not in self.readline(): 109 | pass 110 | 111 | def setStdin(self, stdin): 112 | r"""!Set the stdin stream instance""" 113 | self._stdin = stdin 114 | 115 | def setStdout(self, stdout): 116 | r"""!Set the stdout stream instance""" 117 | self._stdout = stdout 118 | 119 | def setStderr(self, stderr): 120 | r"""!Set the stderr stream instance""" 121 | self._stderr = stderr 122 | 123 | @property 124 | def stdout(self): 125 | r"""!Property to get the stdout stream""" 126 | return self._processHandle.stdout 127 | 128 | @property 129 | def stderr(self): 130 | r"""!Property to get the stderr stream""" 131 | return self._processHandle.stderr 132 | 133 | @property 134 | def isRunning(self): 135 | r"""!Property to get process running state 136 | 137 | @return True or False""" 138 | if self._processHandle: 139 | if self._processHandle.poll() is None: 140 | return True 141 | return False 142 | -------------------------------------------------------------------------------- /docu/docs/install.md: -------------------------------------------------------------------------------- 1 | # 🇩🇪 Anleitung zur Installation von BOSWatch3 2 | Die Installation von BOSWatch3 wird mittels diesem bash-Skript weitestgehend automatisiert durchgeführt. 3 | ## 1. Installationsskript herunterladen 4 | Zunächst wird das aktuelle Installationsskript heruntergeladen 5 | Öffne ein Terminal und führe folgenden Befehl aus: 6 | 7 | ```bash 8 | wget https://github.com/BOSWatch/BW3-Core/raw/master/install.sh 9 | ``` 10 | 11 | ## 2. Installationsskript ausführen 12 | Im Anschluss wird das Skript mit dem Kommando 13 | 14 | ```bash 15 | sudo bash install.sh 16 | ``` 17 | 18 | ausgeführt. 19 | 20 | ### 2a. Optionale Parameter beim Installieren 21 | Standardmäßig wird das Programm nach /opt/boswatch3 installiert. Folgende Parameter stehen zur Installation zur Verfügung: 22 | 23 | | Parameter | Zulässige Werte | Beschreibung | 24 | | ---------------- | ----------------------- | --------------------------------------------------------------------------------------------------- | 25 | | `-r`, `--reboot` | *(kein Wert notwendig)* | Führt nach der Installation automatisch einen Neustart durch. Ohne Angabe erfolgt **kein** Reboot. | 26 | | `-b`, `--branch` | `master`, `develop` | Wählt den zu installierenden Branch. `master` ist stabil (empfohlen), `develop` ist für Entwickler. | 27 | | `-p`, `--path` | z. B. `/opt/boswatch3` | Installiert BOSWatch3 in ein anderes Verzeichnis (**nicht empfohlen**). Standard ist `/opt/boswatch3`. | 28 | 29 | **ACHTUNG:** 30 | 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. 31 | 32 | Falls eine Installation mit Parameter gewünscht wird, so kann dies wie in folgendem Beispiel gestartet werden: 33 | 34 | ```bash 35 | sudo bash install.sh --branch master --path /opt/boswatch3 --reboot 36 | ``` 37 | 38 | ## 3. Konfiguration nach der Installation 39 | Nach der Installation muss die Konfiguration der Dateien `/opt/boswatch3/config/client.yaml` 40 | und `/opt/boswatch3/config/server.yaml` angepasst werden (z.B. mit nano, WinSCP,...): 41 | 42 | ```bash 43 | sudo nano /opt/boswatch3/config/client.yaml 44 | ``` 45 | 46 | und 47 | 48 | ```bash 49 | sudo nano /opt/boswatch3/config/server.yaml 50 | ``` 51 | 52 | Passe die Einstellungen nach deinen Anforderungen an. Bei einem Upgrade einer bestehenden Version kann dieser Schritt ggf. entfallen. 53 | 54 | **INFORMATION:** 55 | Weitere Informationen zur Konfiguration: 56 | [Konfiguration](/docu/docs/config.md) 57 | 58 | ## 4. Neustart 59 | **WICHTIG:** 60 | Bitte starte das System neu, bevor du BOSWatch3 zum ersten Mal startest! 61 | 62 | ```bash 63 | sudo reboot 64 | ``` 65 | 66 | ## 5. Start von BOSWatch3 67 | weiter gehts bei [BOSWatch benutzen](usage.md) 68 | 69 | --- 70 | 71 | # 🇬🇧 BOSWatch3 Installation Guide 72 | The installation of BOSWatch3 is largely automated using this bash script. 73 | 74 | ## 1. Download the Installation Script 75 | First, download the latest installation script. 76 | Open a terminal and run the following command: 77 | 78 | ```bash 79 | wget https://github.com/BOSWatch/BW3-Core/raw/master/install.sh 80 | ``` 81 | 82 | ## 2. Run the Installation Script 83 | Then run the script with the command: 84 | 85 | ```bash 86 | sudo bash install.sh 87 | ``` 88 | 89 | ### 2a. Optional Parameters for Installation 90 | By default, the program is installed to `/opt/boswatch3`. The following parameters are available for installation: 91 | 92 | | Parameter | Allowed Values | Description | 93 | | ---------------- | -----------------------| --------------------------------------------------------------------------------------------------- | 94 | | `-r`, `--reboot` | *(no value needed)* | Automatically reboots the system after installation. Without this, **no** reboot will be performed. | 95 | | `-b`, `--branch` | `master`, `develop` | Selects the branch to install. `master` is stable (recommended), `develop` is for developers. | 96 | | `-p`, `--path` | e.g. `/opt/boswatch3` | Installs BOSWatch3 to a different directory (**not recommended**). Default is `/opt/boswatch3`. | 97 | 98 | **WARNING:** 99 | 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. 100 | 101 | If you want to install with parameters, you can run the following example command: 102 | 103 | ```bash 104 | sudo bash install.sh --branch master --path /opt/boswatch3 --reboot 105 | ``` 106 | 107 | ## 3. Configuration After Installation 108 | After installation, the configuration files `/opt/boswatch3/config/client.yaml` 109 | and `/opt/boswatch3/config/server.yaml` must be adjusted (e.g. using nano, WinSCP, ...): 110 | 111 | ```bash 112 | sudo nano /opt/boswatch3/config/client.yaml 113 | ``` 114 | 115 | and 116 | 117 | ```bash 118 | sudo nano /opt/boswatch3/config/server.yaml 119 | ``` 120 | 121 | Adjust the settings according to your requirements. If upgrading from an existing version, this step might be skipped. 122 | 123 | **INFORMATION:** 124 | More information about configuration: 125 | [Configuration](/docu/docs/config.md) 126 | 127 | ## 4. Reboot 128 | **IMPORTANT:** 129 | Please reboot the system before starting BOSWatch3 for the first time! 130 | 131 | ```bash 132 | sudo reboot 133 | ``` 134 | 135 | ## 5. Starting BOSWatch3 136 | Continue with [Using BOSWatch](usage.md) -------------------------------------------------------------------------------- /plugin/divera.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: divera.py 13 | @date: 16.01.2022 14 | @author: Lars Gremme 15 | @description: Divera247 Plugin 16 | """ 17 | import logging 18 | from plugin.pluginBase import PluginBase 19 | 20 | # ###################### # 21 | # Custom plugin includes # 22 | import asyncio 23 | from aiohttp import ClientSession 24 | import urllib 25 | # ###################### # 26 | 27 | logging.debug("- %s loaded", __name__) 28 | 29 | 30 | class BoswatchPlugin(PluginBase): 31 | r"""!Description of the Plugin""" 32 | def __init__(self, config): 33 | r"""!Do not change anything here!""" 34 | super().__init__(__name__, config) # you can access the config class on 'self.config' 35 | 36 | def fms(self, bwPacket): 37 | r"""!Called on FMS alarm 38 | 39 | @param bwPacket: bwPacket instance 40 | Remove if not implemented""" 41 | fms_data = self.config.get("fms") 42 | apicall = urllib.parse.urlencode({ 43 | "accesskey": self.config.get("accesskey", default=""), 44 | "vehicle_ric": self.parseWildcards(fms_data.get("vehicle", default="")), 45 | "status_id": bwPacket.get("status"), 46 | "status_note": bwPacket.get("directionText"), 47 | "title": self.parseWildcards(fms_data.get("title", default="{FMS}")), 48 | "text": self.parseWildcards(fms_data.get("message", default="{FMS}")), 49 | "priority": fms_data.get("priority", default="false"), 50 | }) 51 | apipath = "/api/fms" 52 | self._makeRequests(apipath, apicall) 53 | 54 | def pocsag(self, bwPacket): 55 | r"""!Called on POCSAG alarm 56 | 57 | @param bwPacket: bwPacket instance 58 | Remove if not implemented""" 59 | poc_data = self.config.get("pocsag") 60 | apicall = urllib.parse.urlencode({ 61 | "accesskey": self.config.get("accesskey", default=""), 62 | "title": self.parseWildcards(poc_data.get("title", default="{RIC}({SRIC})\n{MSG}")), 63 | "ric": self.parseWildcards(poc_data.get("ric", default="")), 64 | "text": self.parseWildcards(poc_data.get("message", default="{MSG}")), 65 | "priority": poc_data.get("priority", default="false"), 66 | }) 67 | apipath = "/api/alarm" 68 | self._makeRequests(apipath, apicall) 69 | 70 | def zvei(self, bwPacket): 71 | r"""!Called on ZVEI alarm 72 | 73 | @param bwPacket: bwPacket instance 74 | Remove if not implemented""" 75 | zvei_data = self.config.get("zvei") 76 | apicall = urllib.parse.urlencode({ 77 | "accesskey": self.config.get("accesskey", default=""), 78 | "title": self.parseWildcards(zvei_data.get("title", default="{TONE}")), 79 | "ric": self.parseWildcards(zvei_data.get("ric", default="{TONE}")), 80 | "text": self.parseWildcards(zvei_data.get("message", default="{TONE}")), 81 | "priority": zvei_data.get("priority", default="false"), 82 | }) 83 | apipath = "/api/alarm" 84 | self._makeRequests(apipath, apicall) 85 | 86 | def msg(self, bwPacket): 87 | r"""!Called on MSG packet 88 | 89 | @param bwPacket: bwPacket instance 90 | Remove if not implemented""" 91 | msg_data = self.config.get("msg") 92 | apicall = urllib.parse.urlencode({ 93 | "accesskey": self.config.get("accesskey", default=""), 94 | "title": self.parseWildcards(msg_data.get("title", default="{MSG}")), 95 | "ric": self.parseWildcards(msg_data.get("ric", default="")), 96 | "text": self.parseWildcards(msg_data.get("message", default="{MSG}")), 97 | "priority": msg_data.get("priority", default="false"), 98 | }) 99 | apipath = "/api/alarm" 100 | self._makeRequests(apipath, apicall) 101 | 102 | def _makeRequests(self, apipath, apicall): 103 | """Parses wildcard urls and handles asynchronus requests 104 | 105 | @param urls: array of urls""" 106 | url = "https://www.divera247.com" 107 | request = url + apipath + "?" + apicall 108 | 109 | loop = asyncio.get_event_loop() 110 | 111 | future = asyncio.ensure_future(self._asyncRequests(request)) 112 | loop.run_until_complete(future) 113 | 114 | async def _asyncRequests(self, url): 115 | """Handles asynchronus requests 116 | 117 | @param urls: array of urls to send requests to""" 118 | tasks = [] 119 | 120 | async with ClientSession() as session: 121 | logging.debug("Generated URL: [{}]".format(url)) 122 | task = asyncio.ensure_future(self._fetch(url, session)) 123 | tasks.append(task) 124 | 125 | responses = asyncio.gather(*tasks) 126 | await responses 127 | 128 | async def _fetch(self, url, session): 129 | """Fetches requests 130 | 131 | @param url: url 132 | 133 | @param session: Clientsession instance""" 134 | logging.debug("Post URL: [{}]".format(url)) 135 | async with session.post(url) as response: 136 | logging.info("{} returned [{}]".format(response.url, response.status)) 137 | return await response.read() 138 | -------------------------------------------------------------------------------- /test/boswatch/test_decoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: test_Decoder.py 13 | @date: 15.12.2017 14 | @author: Bastian Schroll 15 | @description: Unittests for BOSWatch. File have to run as "pytest" unittest 16 | """ 17 | # problem of the pytest fixtures 18 | # pylint: disable=redefined-outer-name 19 | import logging 20 | 21 | from boswatch.decoder.decoder import Decoder 22 | 23 | 24 | def setup_function(function): 25 | logging.debug("[TEST] %s.%s", function.__module__, function.__name__) 26 | 27 | 28 | def test_decoderNoData(): 29 | r"""!Test a empty string""" 30 | assert Decoder.decode("") is None 31 | 32 | 33 | def test_decoderZveiValid(): 34 | r"""!Test valid ZVEI""" 35 | assert not Decoder.decode("ZVEI1: 12345") is None 36 | assert not Decoder.decode("ZVEI1: 12838") is None 37 | assert not Decoder.decode("ZVEI1: 34675") is None 38 | 39 | 40 | def test_decoderZveiDoubleTone(): 41 | r"""!Test doubleTone included ZVEI""" 42 | assert not Decoder.decode("ZVEI1: 6E789") is None 43 | assert not Decoder.decode("ZVEI1: 975E7") is None 44 | assert not Decoder.decode("ZVEI1: 2E87E") is None 45 | 46 | 47 | def test_decoderZveiInvalid(): 48 | """Test invalid ZVEI""" 49 | assert Decoder.decode("ZVEI1: 1245A") is None 50 | assert Decoder.decode("ZVEI1: 1245") is None 51 | assert Decoder.decode("ZVEI1: 135") is None 52 | assert Decoder.decode("ZVEI1: 54") is None 53 | assert Decoder.decode("ZVEI1: 54") is None 54 | 55 | 56 | def test_decoderPocsagValid(): 57 | r"""!Test valid POCSAG""" 58 | assert not Decoder.decode("POCSAG512: Address: 1000000 Function: 0") is None 59 | assert not Decoder.decode("POCSAG512: Address: 1000001 Function: 1") is None 60 | assert not Decoder.decode("POCSAG1200: Address: 1000002 Function: 2") is None 61 | assert not Decoder.decode("POCSAG2400: Address: 1000003 Function: 3") is None 62 | 63 | 64 | def test_decoderPocsagText(): 65 | r"""!Test POCSAG with text""" 66 | assert not Decoder.decode("POCSAG512: Address: 1000000 Function: 0 Alpha: test") is None 67 | assert not Decoder.decode("POCSAG512: Address: 1000001 Function: 1 Alpha: test") is None 68 | assert not Decoder.decode("POCSAG1200: Address: 1000002 Function: 2 Alpha: test") is None 69 | assert not Decoder.decode("POCSAG2400: Address: 1000003 Function: 3 Alpha: test") is None 70 | 71 | 72 | def test_decoderPocsagShortRic(): 73 | r"""!Test short POCSAG""" 74 | assert not Decoder.decode("POCSAG512: Address: 3 Function: 0 Alpha: test") is None 75 | assert not Decoder.decode("POCSAG512: Address: 33 Function: 0 Alpha: test") is None 76 | assert not Decoder.decode("POCSAG1200: Address: 333 Function: 0 Alpha: test") is None 77 | assert not Decoder.decode("POCSAG1200: Address: 3333 Function: 0 Alpha: test") is None 78 | assert not Decoder.decode("POCSAG2400: Address: 33333 Function: 0 Alpha: test") is None 79 | assert not Decoder.decode("POCSAG2400: Address: 333333 Function: 0 Alpha: test") is None 80 | assert not Decoder.decode("POCSAG2400: Address: 3333333 Function: 0 Alpha: test") is None 81 | 82 | 83 | def test_decoderPocsagInvalid(): 84 | r"""!Test invalid POCSAG""" 85 | assert Decoder.decode("POCSAG512: Address: 333333F Function: 0 Alpha: invalid") is None 86 | assert Decoder.decode("POCSAG512: Address: 333333F Function: 1 Alpha: invalid") is None 87 | assert Decoder.decode("POCSAG512: Address: 3333333 Function: 4 Alpha: invalid") is None 88 | 89 | 90 | def test_decoderFmsValid(): 91 | r"""!Test valid FMS""" 92 | assert not Decoder.decode("""FMS: 43f314170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Status 3=Einsatz Ab 0=FZG->LST 2=I (ohneNA,ohneSIGNAL)) CRC correct""") is None 93 | assert not Decoder.decode("""FMS: 43f314170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Status 3=Einsatz Ab 1=LST->FZG 2=I (ohneNA,ohneSIGNAL)) CRC correct""") is None 94 | assert not Decoder.decode("""FMS: 43f314170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Status 3=Einsatz Ab 0=FZG->LST 2=II (ohneNA,mit SIGNAL)) CRC correct""") is None 95 | assert not Decoder.decode("""FMS: 43f314170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Status 3=Einsatz Ab 1=LST->FZG 2=III(mit NA,ohneSIGNAL)) CRC correct""") is None 96 | assert not Decoder.decode("""FMS: 43f314170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Status 3=Einsatz Ab 0=FZG->LST 2=IV (mit NA,mit SIGNAL)) CRC correct""") is None 97 | 98 | 99 | def test_decoderFmsInvalid(): 100 | r"""!Test invalid FMS""" 101 | assert Decoder.decode("""FMS: 14170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Status 3=Einsatz Ab 1=LST->FZG 2=III(mit NA,ohneSIGNAL)) CRC correct""") is None 102 | assert Decoder.decode("""FMS: 43f314170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Sta 3=Einsatz Ab 0=FZG->LST 2=IV (mit NA,mit SIGNAL)) CRC correct""") is None 103 | assert Decoder.decode("""FMS: 14170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Status 3=Einsatz Ab 1=LST->FZG 2=III(mit NA,ohneSIGNAL)) CRC incorrect""") is None 104 | assert Decoder.decode("""FMS: 43f314170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Sta 3=Einsatz Ab 0=FZG->LST 2=IV (mit NA,mit SIGNAL)) CRC incorrect""") is None 105 | -------------------------------------------------------------------------------- /test/testdata.list: -------------------------------------------------------------------------------- 1 | # Testdata for the BOSWatch Test Mode function 2 | # Data in Multimon-NG Raw Format 3 | # Data is alternately passed to the decoder to simulate an used Radio-Frequency 4 | 5 | # 6 | # POCSAG 7 | # ------ 8 | # 9 | # The following settings in config.ini are expected for POCSAG 10 | # 11 | # [BOSWatch] 12 | # useDescription = 1 13 | # doubleFilter_ignore_entries = 10 14 | # doubleFilter_check_msg = 1 15 | # 16 | # [POC] 17 | # deny_ric = 7777777 18 | # filter_range_start = 0000005 19 | # filter_range_end = 8999999 20 | # idDescribed = 1 21 | # 22 | 23 | # bitrate 24 | POCSAG512: Address: 1000512 Function: 1 Alpha: BOSWatch-Test ÖÄÜß: okay 25 | POCSAG1200: Address: 1001200 Function: 1 Alpha: BOSWatch-Test: okay 26 | POCSAG2400: Address: 1002400 Function: 1 Alpha: BOSWatch-Test: okay 27 | 28 | # function-code 29 | POCSAG512: Address: 1000000 Function: 0 Alpha: BOSWatch-Test: okay 30 | POCSAG512: Address: 1000001 Function: 1 Alpha: BOSWatch-Test: okay 31 | POCSAG512: Address: 1000002 Function: 2 Alpha: BOSWatch-Test: okay 32 | POCSAG512: Address: 1000003 Function: 3 Alpha: BOSWatch-Test: okay 33 | 34 | # german special sign 35 | POCSAG512: Address: 1200001 Function: 1 Alpha: BOSWatch-Test ÖÄÜß: okay 36 | POCSAG512: Address: 1200001 Function: 1 Alpha: BOSWatch-Test öäü: okay 37 | 38 | # with csv 39 | POCSAG512: Address: 1234567 Function: 1 Alpha: BOSWatch-Test: with csv 40 | 41 | # without csv 42 | POCSAG1200: Address: 2345678 Function: 2 Alpha: BOSWatch-Test: without csv 43 | POCSAG2400: Address: 3456789 Function: 3 Alpha: BOSWatch-Test: without csv 44 | 45 | # OHNE TEXT???? 46 | POCSAG1200: Address: 1100000 Function: 0 47 | POCSAG1200: Address: 1100000 Function: 1 48 | POCSAG1200: Address: 1100000 Function: 2 49 | POCSAG1200: Address: 1100000 Function: 3 50 | 51 | # duplicate with same and other msg 52 | POCSAG1200: Address: 2000001 Function: 2 Alpha: BOSWatch-Test: second is a duplicate 53 | POCSAG1200: Address: 2000001 Function: 2 Alpha: BOSWatch-Test: second is a duplicate 54 | POCSAG1200: Address: 2000001 Function: 2 Alpha: BOSWatch-Testing: okay 55 | 56 | # duplicate in different order 57 | POCSAG1200: Address: 2100000 Function: 2 58 | POCSAG1200: Address: 2100001 Function: 2 59 | POCSAG1200: Address: 2100002 Function: 2 60 | POCSAG1200: Address: 2100000 Function: 2 61 | POCSAG1200: Address: 2100001 Function: 2 62 | POCSAG1200: Address: 2100002 Function: 2 63 | POCSAG1200: Address: 2100000 Function: 2 Alpha: BOSWatch-Test: second is a duplicate 64 | POCSAG1200: Address: 2100001 Function: 2 Alpha: BOSWatch-Test: second is a duplicate 65 | POCSAG1200: Address: 2100002 Function: 2 Alpha: BOSWatch-Test: second is a duplicate 66 | POCSAG1200: Address: 2100000 Function: 2 Alpha: BOSWatch-Test: second is a duplicate 67 | POCSAG1200: Address: 2100001 Function: 2 Alpha: BOSWatch-Test: second is a duplicate 68 | POCSAG1200: Address: 2100002 Function: 2 Alpha: BOSWatch-Test: second is a duplicate 69 | 70 | # invalid 71 | POCSAG512: Address: 3 Function: 0 Alpha: BOSWatch-Test: okay 72 | POCSAG512: Address: 33 Function: 0 Alpha: BOSWatch-Test: okay 73 | POCSAG512: Address: 333 Function: 0 Alpha: BOSWatch-Test: okay 74 | POCSAG512: Address: 3333 Function: 0 Alpha: BOSWatch-Test: okay 75 | POCSAG512: Address: 33333 Function: 0 Alpha: BOSWatch-Test: okay 76 | POCSAG512: Address: 333333 Function: 0 Alpha: BOSWatch-Test: okay 77 | POCSAG512: Address: 3333333 Function: 0 Alpha: BOSWatch-Test: okay 78 | POCSAG512: Address: 333333F Function: 0 Alpha: BOSWatch-Test: invalid 79 | POCSAG512: Address: 333333F Function: 1 Alpha: BOSWatch-Test: invalid 80 | POCSAG512: Address: 3333333 Function: 4 Alpha: BOSWatch-Test: invalid 81 | 82 | # denied 83 | POCSAG1200: Address: 7777777 Function: 1 Alpha: BOSWatch-Test: denied 84 | 85 | # out of filter Range 86 | POCSAG1200: Address: 0000004 Function: 1 Alpha: BOSWatch-Test: out of filter start 87 | POCSAG1200: Address: 9000000 Function: 1 Alpha: BOSWatch-Test: out of filter end 88 | 89 | #Probealram 90 | POCSAG1200: Address: 0871004 Function: 1 Alpha: Dies ist ein Probealarm! 91 | ## Multicast Alarm 92 | POCSAG1200: Address: 0871002 Function: 0 Alpha: 93 | POCSAG1200: Address: 0860001 Function: 0 94 | POCSAG1200: Address: 0860002 Function: 0 95 | POCSAG1200: Address: 0860003 Function: 0 96 | POCSAG1200: Address: 0860004 Function: 0 97 | POCSAG1200: Address: 0860005 Function: 0 98 | POCSAG1200: Address: 0860006 Function: 0 99 | POCSAG1200: Address: 0860007 Function: 0 100 | POCSAG1200: Address: 0860008 Function: 0 101 | POCSAG1200: Address: 0860009 Function: 0 102 | POCSAG1200: Address: 0860010 Function: 0 103 | POCSAG1200: Address: 0871003 Function: 0 Alpha: B2 Feuer Gebäude Pers in Gefahr. bla bla bla 104 | 105 | # regEx-Filter? 106 | 107 | 108 | # 109 | # FMS 110 | # --- 111 | # 112 | FMS: 43f314170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Status 3=Einsatz Ab 0=FZG->LST 2=I (ohneNA,ohneSIGNAL)) CRC correct 113 | FMS: 43f314170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Status 3=Einsatz Ab 1=LST->FZG 2=I (ohneNA,ohneSIGNAL)) CRC correct 114 | FMS: 43f314170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Status 3=Einsatz Ab 0=FZG->LST 2=II (ohneNA,mit SIGNAL)) CRC correct 115 | FMS: 43f314170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Status 3=Einsatz Ab 1=LST->FZG 2=III(mit NA,ohneSIGNAL)) CRC correct 116 | FMS: 43f314170000 (9=Rotkreuz 3=Bayern 1 Ort 0x25=037FZG 7141Status 3=Einsatz Ab 0=FZG->LST 2=IV (mit NA,mit SIGNAL)) CRC correct 117 | 118 | 119 | # 120 | # ZVEI 121 | # ---- 122 | # 123 | 124 | #with csv description 125 | ZVEI1: 12345 126 | #without csv description 127 | ZVEI1: 56789 128 | #duplicate 129 | ZVEI1: 56789 130 | #with repeat Tone 131 | ZVEI1: 1F2E3 132 | #in case of invalid id 133 | ZVEI1: 135 134 | #in case of a double-tone for siren n-'D's are sended 135 | # ZVEI1: DDD 136 | # ZVEI1: DDDDD 137 | -------------------------------------------------------------------------------- /docu/docs/service.md: -------------------------------------------------------------------------------- 1 | # BOSWatch – Dienstinstallation (Service Setup) 2 | 3 | ## 🇩🇪 BOSWatch als Dienst verwenden 4 | Es wird vorausgesetzt, dass BOSWatch unter `/opt/boswatch3` installiert ist. 5 | Falls du einen anderen Installationspfad nutzt, müssen alle Pfadangaben in dieser Anleitung, im Skript sowie in den generierten Service-Dateien entsprechend angepasst werden. 6 | 7 | Für jeden Dienst muss eine eigene `*.yaml`-Datei im Ordner `config/` vorhanden sein. 8 | Beim Ausführen des Skripts wirst du interaktiv gefragt, welche dieser YAML-Dateien installiert oder übersprungen werden sollen. 9 | 10 | ### Dienst installieren 11 | Als Erstes wechseln wir ins BOSWatch Verzeichnis: 12 | 13 | ```bash 14 | cd /opt/boswatch3 15 | ``` 16 | 17 | Das Installationsskript `install_service.py` wird anschließend mit Root-Rechten ausgeführt: 18 | ```bash 19 | sudo python3 install_service.py 20 | ``` 21 | Es folgt ein interaktiver Ablauf, bei dem du gefragt wirst, welche YAML-Dateien installiert oder entfernt werden sollen. 22 | 23 | ### Zusätzliche Optionen (fortgeschrittene Anwender) 24 | Das Skript bietet zusätzliche CLI-Optionen für mehr Kontrolle: 25 | 26 | ```bash 27 | usage: install_service.py [-h] [--verbose] [--quiet] 28 | 29 | Installiert oder entfernt systemd-Services für BOSWatch basierend auf YAML-Konfigurationsdateien. 30 | 31 | optional arguments: 32 | -h, --help zeigt diese Hilfe an 33 | --dry-run für Entwickler: führt keine echten Änderungen aus (Simulation) 34 | --verbose zeigt ausführliche Debug-Ausgaben 35 | --quiet unterdrückt alle Ausgaben außer Warnungen und Fehlern 36 | -l, --lang [de|en] Sprache für alle Ausgaben (Standard: de) 37 | ``` 38 | 39 | ### Neustart nach Serviceinstallation 40 | Nach Durchlaufen des Skripts boote dein System erneut durch, um den korrekten Startvorgang zu überprüfen: 41 | ```bash 42 | sudo reboot 43 | ``` 44 | 45 | ### Kontrolle, ob alles funktioniert hat 46 | Um zu kontrollieren, ob alles ordnungsgemäß hochgefahren ist, kannst du die zwei Services mit folgenden Befehlen abfragen und die letzten Log-Einträge ansehen: 47 | 48 | 1. Client-Service 49 | ```bash 50 | sudo systemctl status bw3_[clientname].service 51 | ``` 52 | 53 | Ersetze [clientname] mit dem Namen, den deine client.yaml hat (Standardmäßig: client) 54 | 55 | Um das Log zu schließen, "q" drücken. 56 | 57 | 2. Server-Service 58 | ```bash 59 | sudo systemctl status bw3_[servername].service 60 | ``` 61 | 62 | Ersetze [servername] mit dem Namen, den deine server.yaml hat (Standardmäßig: server) 63 | 64 | Um das Log zu schließen, "q" drücken. 65 | 66 | **Beide Outputs sollten so ähnlich beginnen:** 67 | ```text 68 | bw3_client.service - BOSWatch Client 69 | Loaded: loaded (/etc/systemd/system/bw3_client.service; enabled; preset: enabled) 70 | Active: active (running) since Mon 1971-01-01 01:01:01 CEST; 15min 53s ago 71 | ``` 72 | 73 | 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. 74 | 75 | ### Logdatei 76 | Alle Aktionen des Installationsskripts werden in der Datei log/install/service_install.log protokolliert. 77 | 78 | ### Hinweis 79 | Nach der Installation oder Entfernung wird systemctl daemon-reexec automatisch aufgerufen, damit systemd die neuen oder entfernten Units korrekt verarbeitet. 80 | 81 | --- 82 | 83 | ## 🇬🇧 Use BOSWatch as a Service 84 | 85 | We assume that BOSWatch is installed to `/opt/boswatch3`. 86 | If you are using a different path, please adjust all paths in this guide, in the script and in the generated service files accordingly. 87 | 88 | Each service requires its own `*.yaml` file inside the `config/` folder. 89 | The script will interactively ask which YAML files to install or skip. 90 | 91 | ### Install the Service 92 | 93 | First, change directory to BOSWatch folder: 94 | 95 | ```bash 96 | cd /opt/boswatch3 97 | ``` 98 | 99 | After that, run the install script `install_service.py` with root permissions: 100 | 101 | ```bash 102 | sudo python3 install_service.py -l en 103 | ``` 104 | 105 | You will be guided through an interactive selection to install or remove desired services. 106 | 107 | ### Additional Options 108 | 109 | The script supports additional CLI arguments for advanced usage: 110 | 111 | ```bash 112 | usage: install_service.py [-h] [--dry-run] [--verbose] [--quiet] 113 | 114 | Installs or removes BOSWatch systemd services based on YAML config files. 115 | 116 | optional arguments: 117 | -h, --help show this help message and exit 118 | --dry-run simulate actions without making real changes 119 | --verbose show detailed debug output 120 | --quiet suppress all output except warnings and errors 121 | -l, --lang [de|en] Language for all output (default: de) 122 | ``` 123 | 124 | ### Reboot After Setup 125 | After running the script, reboot your system to verify that the services start correctly: 126 | ```bash 127 | sudo reboot 128 | ``` 129 | 130 | ### Verifying Successful Setup 131 | To check if everything started properly, you can query the two services and inspect the most recent log entries: 132 | 133 | 1. Client Service 134 | ```bash 135 | sudo systemctl status bw3_[clientname].service 136 | ``` 137 | 138 | Replace [clientname] with the name of your client.yaml file (default: client). 139 | 140 | To close the log, press "q". 141 | 142 | 2. Server Service 143 | ```bash 144 | sudo systemctl status bw3_[servername].service 145 | ``` 146 | 147 | Replace [servername] with the name of your server.yaml file (default: server). 148 | 149 | To close the log, press "q". 150 | 151 | **Both outputs should start similarly to this:** 152 | ```text 153 | bw3_client.service - BOSWatch Client 154 | Loaded: loaded (/etc/systemd/system/bw3_client.service; enabled; preset: enabled) 155 | Active: active (running) since Mon 1971-01-01 01:01:01 CEST; 15min 53s ago 156 | ``` 157 | 158 | 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. 159 | 160 | ### Log File 161 | 162 | All actions of the installation script are logged to `log/install/service_install.log`. 163 | 164 | ### Note 165 | 166 | After installation or removal, `systemctl daemon-reexec` is automatically triggered to reload unit files properly. 167 | -------------------------------------------------------------------------------- /docu/docs/develop/ModulPlugin.md: -------------------------------------------------------------------------------- 1 | #
Eigenes Modul/Plugin schreiben
2 | 3 | Um ein eigenes Modul oder Plugin zu schreiben, sollte man sich am besten zuerst einmal das das `template` im entsprechenden Ordner ansehen. Dies kann als Vorlage für das eigene Modul oder Plugin genutzt werden. 4 | 5 | --- 6 | ## Allgemeine Informationen 7 | Im ersten Schritt sollte eine Kopie des jeweiligen Templates (Modul oder Plugin) erstellt werden. Nun sollten im Dateikopf die Angaben angepasst werden. 8 | 9 | --- 10 | ## Benötigte Methoden überschreiben 11 | ### Modul 12 | Die Modul Basisklasse bietet einige Methoden, welche vom Modul überschrieben werden können. 13 | 14 | - `onLoad()` wird direkt beim Import des Moduls ausgeführt 15 | - `doWork(bwPacket)` wird bei der Ausführung aufgerufen 16 | - `onUnload()` wird beim Zerstören der Plugin Modul zum Programmende ausgeführt 17 | 18 | ### Plugin 19 | Die Plugin Basisklasse bietet einige Methoden, welche vom Plugin überschrieben werden können. 20 | 21 | - `onLoad()` wird direkt beim Import des Plugins ausgeführt 22 | - `setup()` wird vor jeder Ausführung gerufen 23 | - `fms(bwPacket)` wird bei einem FMS Paket ausgeführt 24 | - `pocsag(bwPacket)` wird bei einem POCSAG Paket ausgeführt 25 | - `zvei(bwPacket)` wird bei einem ZVEI Packet ausgeführt 26 | - `msg(bwPacket)` wird bei einem Nachrichten Packet ausgeführt 27 | - `teardown()` wird nach jeder Ausführung gerufen 28 | - `onUnload()` wird beim Zerstören der Plugin Instanz zum Programmende ausgeführt 29 | 30 | --- 31 | ## Konfiguration 32 | ### Konfiguration anlegen 33 | Jedes Modul oder Plugin wird in einem Router folgendermaßen deklariert: 34 | ```yaml 35 | - type: module # oder 'plugin' 36 | res: template_module # Name der Python Datei (ohne .py) 37 | name: Mein Modul # optionaler Name 38 | config: # config-Sektion 39 | option1: value 1 40 | option2: 41 | underOption1: value 21 42 | underOption2: value 22 43 | list: 44 | - list 1 45 | - list 2 46 | ``` 47 | Eine entsprechende Dokumentation der Parameter **muss** in der Dokumentation des jeweiligen Moduls oder Plugins hinterleget werden. 48 | 49 | ### Konfiguration verwenden 50 | Wird der Instanz eine Konfiguration übergeben wird diese in `self.config` abgelegt und kann wie folgt abgerufen werden: 51 | (Dies Ergebnisse beziehen sich auf das Konfigurationsbeispiel oben) 52 | 53 | - Einzelnes Feld auslesen 54 | `self.config.get("option1")` 55 | > liefert `value 1` 56 | 57 | - Verschachteltes Feld auslesen (beliebige tiefe möglich) 58 | `self.config.get("option2", "underOption1")` 59 | > liefert `value 21` 60 | 61 | - Es kann ein Default Wert angegeben werden (falls entsprechender Eintrag fehlt) 62 | `self.config.get("notSet", default="defValue")` 63 | > liefert `defValue` 64 | 65 | - Über Listen kann einfach iteriert werden 66 | `for item in self.config.get(FIELD):` 67 | > liefert ein Element je Iteration - hier `list 1` und `list 2` 68 | 69 | Wird ein End-Wert ausgelesen, wird dieser direkt zurück gegeben. 70 | Sollten weitere Unterelemente oder eine Liste exisitieren wird erneut ein Objekt der Klasse `Config()` zurück gegeben, auf welches wiederum nach obigem Schema zugegriffen werden kann. 71 | 72 | --- 73 | ## Arbeiten mit dem bwPacket 74 | An das Modul bzw. Plugin wird eine Instanz eines BOSWatch-Paket Objekts übergeben. 75 | Aus dieser kann mittels `bwPacket.get(FIELDNAME)` das entsprechende Feld ausgelesen werden. 76 | Mittels `bwPacket.set(FIELDNAME, VALUE)` kann ein Wert hinzugefügt oder modifiziert werden. 77 | Eine Auflistung der bereitgestellten Informationen findet sich im entsprechenden [BOSWatch Paket](packet.md) Dokumentation. 78 | 79 | **Bitte beachten:** 80 | 81 | - Selbst vom Modul hinzugefügte Felder **müssen** in der Modul Dokumentation unter `Paket Modifikation` aufgeführt werden. 82 | - Sollte ein Modul oder Plugin Felder benutzen, welche in einem anderen Modul erstellt werden, **muss** dies im Punkt `Abhänigkeiten` des jeweiligen Moduls oder Plugins dokumentiert werden. 83 | 84 | ### Rückgabewert bei Modulen 85 | Module können Pakete beliebig verändern. Diese Änderungen werden im Router entsprechend weitergeleitet. 86 | 87 | Mögliche Rückgabewerte eines Moduls: 88 | 89 | - `return bwPacket` Gibt das modifizierte bwPacket an den Router zurück (Paket Modifikation) 90 | - `return None` Der Router fährt mit dem unveränderten bwPacket fort (Input = Output) 91 | - `return False` Der Router stopt sofort die Ausführung (zB. in Filtern verwendet) 92 | 93 | ### Rückgabewert bei Plugins 94 | Plugins geben keine Pakete mehr zurück. Sie fungieren ausschließlich als Endpunkt. 95 | Die Plugin Basisklasse liefert intern immer ein `None` an den Router zurück, 96 | was zur weiteren Ausführung des Routers mit dem original Paket führt. Daher macht es in Plugins keinen Sinn ein Paket zu modifizieren. 97 | 98 | --- 99 | ## Nutzung der Wildcards 100 | 101 | Es gibt einige vordefinierte Wildcards welche in der [BOSWatch Paket](packet.md) Dokumentation zu finden sind. 102 | 103 | Außerdem sind die folgenden allgemeinen Wildcards definiert: 104 | 105 | - `{BR}` - Zeilenumbruch `\r\n` 106 | - `{LPAR}` - öffnende Klammer `(` 107 | - `{RPAR}` - schließende Klammer `)` 108 | - `{TIME}` - Aktueller Zeitstempel im Format `%d.%m.%Y %H:%M:%S` 109 | 110 | ### Wildcards registrieren [Module] 111 | Module können zusätzliche Wildcards registrieren welche anschließend in den Plugins ebenfalls geparst werden können. 112 | Dies kann über die interne Methode `self.registerWildcard(newWildcard, bwPacketField)` gemacht werden. 113 | 114 | - `newWildcard` muss im folgenden Format angegeben werden: `{WILDCARD}` 115 | - `bwPacketField` ist der Name des Feldes im bwPacket - gestezt per `bwPacket.set(FIELDNAME, VALUE)` 116 | 117 | **Bitte beachten:** 118 | 119 | - Selbst vom Modul registrierte Wildcards **müssen** in der Modul Dokumentation unter `Zusätzliche Wildcards` aufgeführt werden. 120 | 121 | ### Wildcards parsen [Plugins] 122 | Das parsen der Wildcars funktioniert komfortabel über die interne Methode `msg = self.parseWildcards(msg)`. 123 | 124 | - `msg` enstrpicht dabei dem String in welchem die Wildcards ersetzt werden sollen 125 | 126 | Die Platzhalter der Wildcards findet man in der [BOSWatch Paket](packet.md) Dokumentation. 127 | 128 | Sollten Module zusätzliche Wildcards registrieren, findet man Informationen dazu in der jeweiligen Modul Dokumentation 129 | 130 | --- 131 | ## Richtiges Logging 132 | tbd ... 133 | -------------------------------------------------------------------------------- /boswatch/network/broadcast.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: broadcast.py 13 | @date: 21.09.2018 14 | @author: Bastian Schroll 15 | @description: UDP broadcast server and client class 16 | """ 17 | import logging 18 | import socket 19 | import threading 20 | 21 | logging.debug("- %s loaded", __name__) 22 | 23 | 24 | class BroadcastClient: 25 | r"""!BroadcastClient class""" 26 | 27 | def __init__(self, port=5000): 28 | r"""!Create an BroadcastClient instance 29 | 30 | @param port: port to send broadcast packets (5000)""" 31 | self._broadcastPort = port 32 | 33 | self._serverIP = "" 34 | self._serverPort = 0 35 | 36 | self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 37 | self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 38 | self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 39 | self._socket.settimeout(3) 40 | 41 | def getConnInfo(self, retry=0): 42 | r"""!Get the connection info from server over udp broadcast 43 | 44 | This function will send broadcast package(s) 45 | to get connection info from the server. 46 | 47 | - send the magic packet on broadcast address. 48 | - wait for a magic packet. 49 | - extract the connection data from the magic packet and return 50 | 51 | @param retry: Count of retry - 0 is infinite (0) 52 | 53 | @return True or False""" 54 | sendPackages = 0 55 | while sendPackages < retry or retry == 0: 56 | try: 57 | logging.debug("send magic as broadcast - Try: %d", sendPackages) 58 | self._socket.sendto("".encode(), ('255.255.255.255', self._broadcastPort)) 59 | sendPackages += 1 60 | payload, address = self._socket.recvfrom(1024) 61 | payload = str(payload, "UTF-8") 62 | 63 | if payload.startswith(""): 64 | logging.debug("received magic from: %s", address[0]) 65 | self._serverIP = address[0] 66 | self._serverPort = int(payload.split(";")[1]) 67 | logging.info("got connection info from server: %s:%d", self._serverIP, self._serverPort) 68 | return True 69 | except socket.timeout: # nothing received - retry 70 | logging.debug("no magic packet received") 71 | logging.warning("cannot fetch connection info after %d tries", sendPackages) 72 | return False 73 | 74 | @property 75 | def serverIP(self): 76 | r"""!Property to get the server IP after successful broadcast""" 77 | return self._serverIP 78 | 79 | @property 80 | def serverPort(self): 81 | r"""!Property to get the server Port after successful broadcast""" 82 | return self._serverPort 83 | 84 | 85 | class BroadcastServer: 86 | r"""!BroadcastServer class""" 87 | 88 | def __init__(self, servePort=8080, listenPort=5000): 89 | r"""!Create an BroadcastServer instance 90 | 91 | @param servePort: port to serve as connection info (8080) 92 | @param listenPort: port to listen for broadcast packets (5000)""" 93 | self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 94 | self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 95 | self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 96 | self._socket.settimeout(2) 97 | self._socket.bind(('', listenPort)) 98 | self._serverThread = None 99 | self._serverShutdown = False 100 | self._servePort = servePort 101 | 102 | def __del__(self): # pragma: no cover 103 | if self.isRunning: 104 | self.stop() 105 | while self.isRunning: 106 | pass 107 | 108 | def start(self): 109 | r"""!Start the broadcast server in a new thread 110 | 111 | @return True or False""" 112 | if not self.isRunning: 113 | logging.debug("start udp broadcast server") 114 | self._serverThread = threading.Thread(target=self._listen) 115 | self._serverThread.name = "BroadServ" 116 | self._serverThread.daemon = True 117 | self._serverShutdown = False 118 | self._serverThread.start() 119 | return True 120 | logging.warning("udp broadcast server always started") 121 | return True 122 | 123 | def stop(self): 124 | r"""!Stop the broadcast server 125 | 126 | Due to the timeout of the socket, 127 | stopping the thread can be delayed by two seconds. 128 | But function returns immediately. 129 | 130 | @return True or False""" 131 | 132 | if self.isRunning: 133 | logging.debug("stop udp broadcast server") 134 | self._serverShutdown = True 135 | return True 136 | else: 137 | logging.warning("udp broadcast server always stopped") 138 | return True 139 | 140 | def _listen(self): 141 | r"""!Broadcast server worker thread 142 | 143 | This function listen for magic packets on broadcast 144 | address and send the connection info to the clients. 145 | 146 | - listen for the magic packet 147 | - send connection info in an macig packet""" 148 | logging.debug("start listening for magic") 149 | while not self._serverShutdown: 150 | try: 151 | payload, address = self._socket.recvfrom(1024) 152 | payload = str(payload, "UTF-8") 153 | if payload == "": 154 | logging.debug("received magic from: %s", address[0]) 155 | logging.info("send connection info in magic to: %s", address[0]) 156 | self._socket.sendto(";".encode() + str(self._servePort).encode(), address) 157 | except socket.timeout: 158 | continue # timeout is accepted (not block at recvfrom()) 159 | self._serverThread = None 160 | logging.debug("udp broadcast server stopped") 161 | 162 | @property 163 | def isRunning(self): 164 | r"""!Property of broadcast server running state""" 165 | if self._serverThread: 166 | return True 167 | return False 168 | -------------------------------------------------------------------------------- /plugin/pluginBase.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | r"""! 4 | ____ ____ ______ __ __ __ _____ 5 | / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / 6 | / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < 7 | / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / 8 | /_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ 9 | German BOS Information Script 10 | by Bastian Schroll 11 | 12 | @file: pluginBase.py 13 | @date: 08.01.2018 14 | @author: Bastian Schroll 15 | @description: Plugin main class to inherit 16 | """ 17 | import logging 18 | import time 19 | from abc import ABC 20 | 21 | from boswatch import wildcard 22 | 23 | logging.debug("- %s loaded", __name__) 24 | 25 | 26 | class PluginBase(ABC): 27 | r"""!Main plugin class""" 28 | 29 | _pluginsActive = [] 30 | 31 | def __init__(self, pluginName, config): 32 | r"""!init preload some needed locals and then call onLoad() directly""" 33 | self._pluginName = pluginName 34 | self.config = config 35 | self._pluginsActive.append(self) 36 | 37 | # to save the packet while alarm is running for other functions 38 | self._bwPacket = None 39 | 40 | # for time counting 41 | self._sumTime = 0 42 | self._cumTime = 0 43 | self._setupTime = 0 44 | self._alarmTime = 0 45 | self._teardownTime = 0 46 | 47 | # for statistics 48 | self._runCount = 0 49 | self._setupErrorCount = 0 50 | self._alarmErrorCount = 0 51 | self._teardownErrorCount = 0 52 | 53 | logging.debug("[%s] onLoad()", pluginName) 54 | self.onLoad() 55 | 56 | def _cleanup(self): 57 | r"""!Cleanup routine calls onUnload() directly""" 58 | logging.debug("[%s] onUnload()", self._pluginName) 59 | self._pluginsActive.remove(self) 60 | self.onUnload() 61 | 62 | def _run(self, bwPacket): 63 | r"""!start an complete running turn of an plugin. 64 | Calls setup(), alarm() and teardown() in this order. 65 | The alarm() method serves the BOSWatch packet to the plugin. 66 | 67 | @param bwPacket: A BOSWatch packet instance""" 68 | self._runCount += 1 69 | logging.debug("[%s] run #%d", self._pluginName, self._runCount) 70 | 71 | self._bwPacket = bwPacket 72 | 73 | tmpTime = time.time() 74 | try: 75 | logging.debug("[%s] setup()", self._pluginName) 76 | self.setup() 77 | except: 78 | self._setupErrorCount += 1 79 | logging.exception("[%s] error in setup()", self._pluginName) 80 | 81 | self._setupTime = time.time() - tmpTime 82 | tmpTime = time.time() 83 | try: 84 | 85 | if bwPacket.get("mode") == "fms": 86 | logging.debug("[%s] fms()", self._pluginName) 87 | self.fms(bwPacket) 88 | if bwPacket.get("mode") == "pocsag": 89 | logging.debug("[%s] pocsag()", self._pluginName) 90 | self.pocsag(bwPacket) 91 | if bwPacket.get("mode") == "zvei": 92 | logging.debug("[%s] zvei()", self._pluginName) 93 | self.zvei(bwPacket) 94 | if bwPacket.get("mode") == "msg": 95 | logging.debug("[%s] msg()", self._pluginName) 96 | self.msg(bwPacket) 97 | except: 98 | self._alarmErrorCount += 1 99 | logging.exception("[%s] alarm error", self._pluginName) 100 | 101 | self._alarmTime = time.time() - tmpTime 102 | tmpTime = time.time() 103 | try: 104 | logging.debug("[%s] teardown()", self._pluginName) 105 | self.teardown() 106 | except: 107 | self._teardownErrorCount += 1 108 | logging.exception("[%s] error in teardown()", self._pluginName) 109 | 110 | self._teardownTime = time.time() - tmpTime 111 | self._sumTime = self._setupTime + self._alarmTime + self._teardownTime 112 | self._cumTime += self._sumTime 113 | 114 | self._bwPacket = None 115 | 116 | logging.debug("[%s] took %0.3f seconds", self._pluginName, self._sumTime) 117 | # logging.debug("- setup: %0.2f sec.", self._setupTime) 118 | # logging.debug("- alarm: %0.2f sec.", self._alarmTime) 119 | # logging.debug("- teardown: %0.2f sec.", self._teardownTime) 120 | 121 | return None 122 | 123 | def _getStatistics(self): 124 | r"""!Returns statistical information's from last plugin run 125 | 126 | @return Statistics as pyton dict""" 127 | stats = {"type": "plugin", 128 | "runCount": self._runCount, 129 | "sumTime": self._sumTime, 130 | "cumTime": self._cumTime, 131 | "setupTime": self._setupTime, 132 | "alarmTime": self._alarmTime, 133 | "teardownTime": self._teardownTime, 134 | "setupErrorCount": self._setupErrorCount, 135 | "alarmErrorCount": self._alarmErrorCount, 136 | "teardownErrorCount": self._teardownErrorCount} 137 | return stats 138 | 139 | def onLoad(self): 140 | r"""!Called by import of the plugin 141 | can be inherited""" 142 | pass 143 | 144 | def setup(self): 145 | r"""!Called before alarm 146 | can be inherited""" 147 | pass 148 | 149 | def fms(self, bwPacket): 150 | r"""!Called on FMS alarm 151 | can be inherited 152 | 153 | @param bwPacket: bwPacket instance""" 154 | logging.warning("ZVEI not implemented in %s", self._pluginName) 155 | 156 | def pocsag(self, bwPacket): 157 | r"""!Called on POCSAG alarm 158 | can be inherited 159 | 160 | @param bwPacket: bwPacket instance""" 161 | logging.warning("POCSAG not implemented in %s", self._pluginName) 162 | 163 | def zvei(self, bwPacket): 164 | r"""!Called on ZVEI alarm 165 | can be inherited 166 | 167 | @param bwPacket: bwPacket instance""" 168 | logging.warning("ZVEI not implemented in %s", self._pluginName) 169 | 170 | def msg(self, bwPacket): 171 | r"""!Called on MSG packet 172 | can be inherited 173 | 174 | @param bwPacket: bwPacket instance""" 175 | logging.warning("MSG not implemented in %s", self._pluginName) 176 | 177 | def teardown(self): 178 | r"""!Called after alarm 179 | can be inherited""" 180 | pass 181 | 182 | def onUnload(self): 183 | r"""!Called on shutdown of boswatch 184 | can be inherited""" 185 | pass 186 | 187 | def parseWildcards(self, msg): 188 | r"""!Return the message with parsed wildcards""" 189 | if self._bwPacket is None: 190 | logging.warning("wildcard replacing not allowed - no bwPacket set") 191 | return msg 192 | return wildcard.replaceWildcards(msg, self._bwPacket) 193 | --------------------------------------------------------------------------------