├── 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 | { .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 | [](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 | 
4 |
5 | 
6 | 
7 | 
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 | { .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 | { .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 | { .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 | { .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 |
--------------------------------------------------------------------------------