Skip to content

Commit

Permalink
rewrite against APRS-IS with aprslib
Browse files Browse the repository at this point in the history
  • Loading branch information
mattmelling committed Feb 16, 2021
1 parent eafc7cd commit bc7abb3
Show file tree
Hide file tree
Showing 15 changed files with 383 additions and 93 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
/build/
/aprs2mqtt/__pycache__/
*.pyc
/result
37 changes: 26 additions & 11 deletions README.org
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
A simple APRS message to MQTT bridge. I consume the messages in Home Assistant and fire out notifications via Telegram.

* Environment
Configuration is done with these environment variables:

- MQTT_HOST
- MQTT_PORT
- MQTT_USER
- MQTT_PASSWORD
- MQTT_TOPIC
- APRSFI_KEY: From your [[http://aprs.fi][aprs.fi]] profile
- APRS_CALLSIGNS: Comman separated list of callsigns to monitor
- LOCK_LOCATION: Location to store timestasmps of last message, I use /tmp.
* Usage
#+BEGIN_SRC bash
python -m aprs2mqtt <config>
#+END_SRC

This repository also supplies a [[https://nixos.wiki/wiki/Flakes][Nix Flake]] which allows configuration with a [[https://nixos.org/][NixOS]] module.

* Configuration
#+BEGIN_SRC yaml
aprs:
host: euro.aprs2.net
port: 14580
login: 2E0YML
mqtt:
host: localhost
user: mqtt
pass: mqtt
port: 1883
consumers:
- filter: t/m
topic: aprs/message
#+END_SRC

Any values under ~aprs~ or ~mqtt~ can be supplied through environment variables such as ~APRS_HOST~ or ~MQTT_PASS~.

The consumers section maps [[http://www.aprs-is.net/javAPRSFilter.aspx][user defined filters]]. Matching messages are dispatched to the respective topic.
16 changes: 16 additions & 0 deletions aprs2mqtt/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from .aprs2mqtt import Aprs2MqttService
from .config import Config
import logging
import plac

def _main(config: ("config", "positional")):
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger('aprs2mqtt')
cfg = Config.from_file(config, logger)
Aprs2MqttService.start(cfg, logger=logger)

def main():
plac.call(_main)

if __name__ == '__main__':
main()
55 changes: 55 additions & 0 deletions aprs2mqtt/aprs2mqtt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import aprslib
import json
import logging
import signal
import sys
from functools import partial
import paho.mqtt.publish as publish

class Aprs2MqttService:
@staticmethod
def start(config, logger=None):
s = Aprs2MqttService(config,
logger=logger)
s.run()

def __init__(self, config, logger=None):
self.logger = (logging.Logger('aprs2mqtt')
if logger is None
else logger)
self.config = config
self.consumers = []
signal.signal(signal.SIGINT, self.stop)
signal.signal(signal.SIGTERM, self.stop)

def handler(self, msg_filter, packet):
self.logger.debug(f'[{msg_filter}] => {packet}')
publish.single(msg_filter['topic'],
payload=json.dumps(packet),
hostname=self.config.get_mqtt_host(),
port=int(self.config.get_mqtt_port()),
client_id=self.config.get_mqtt_user(),
auth={
'username': self.config.get_mqtt_user(),
'password': self.config.get_mqtt_pass()
})

def run(self):
login = self.config.get_aprs_login()
host = self.config.get_aprs_host()
port = self.config.get_aprs_port()
for f in self.config.get_consumers():
rule = f['filter']
topic = f['topic']
self.logger.info(f'adding consumer {login}@{host}:{port} - {rule} => {topic}')
ais = aprslib.IS(login, host=host, port=port)
ais.connect()
ais.set_filter(rule)
ais.consumer(partial(self.handler, f), raw=False)
self.consumers.append(ais)

def stop(self, signal, frame):
self.logger.info(f'Caught signal {signal}, exiting')
for ais in self.consumers:
consumer.close()
sys.exit(0);
54 changes: 54 additions & 0 deletions aprs2mqtt/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import os
import logging
import yaml
try:
from yaml import CLoader as Loader, CDumper as Dumper
except ImportError:
from yaml import Loader, Dumper

class Config:
@staticmethod
def from_file(filename, logger=None):
logger.info(f'Config.from_file({filename})')
with open(filename, 'r') as f:
return Config(yaml.load(f, Loader=Loader), logger)

def __init__(self, config, logger=None):
self._config = config
self._logger = logger if logger is not None else logging.getLogger()

def get_config(self, section, name):
self._logger.debug(f'Config.get_config {section}.{name}')
if section in self._config and name in self._config[section]:
return self._config[section][name]
env = f'{section.upper()}_{name.upper()}'
if env in os.environ:
return os.environ[env]
return None

def get_aprs_host(self):
return self.get_config('aprs', 'host')

def get_aprs_port(self):
return self.get_config('aprs', 'port')

def get_aprs_login(self):
return self.get_config('aprs', 'login')

def get_mqtt_host(self):
return self.get_config('mqtt', 'host')

def get_mqtt_port(self):
return self.get_config('mqtt', 'port')

def get_mqtt_user(self):
return self.get_config('mqtt', 'user')

def get_mqtt_pass(self):
return self.get_config('mqtt', 'pass')

def get_consumers(self):
self._logger.debug('Config.get_consumers')
if 'consumers' in self._config:
return self._config['consumers']
return []
65 changes: 0 additions & 65 deletions aprs2mqtt/main.py

This file was deleted.

70 changes: 70 additions & 0 deletions aprs2mqtt/test_aprs2mqtt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import json
import unittest
from unittest.mock import patch, Mock, MagicMock, DEFAULT
from .config import Config

class TestAprs(unittest.TestCase):

@patch('paho.mqtt.publish.single')
def test_handler(self, mock_publish):
from .aprs2mqtt import Aprs2MqttService
mock_publish = MagicMock()
config = Config({
'mqtt': {
'host': 'localhost',
'port': 1883,
'user': 'user',
'pass': 'pass'
}
})
svc = Aprs2MqttService(config)
svc.handler({ 'topic': 'topic' },
{ 'message': 'ok' })
mock_publish.asset_called_with(
'topic',
payload=json.dumps({ 'message': 'ok' }),
hostname='localhost',
port=1883,
client_id='user',
auth={
'username': 'user',
'password': 'pass'
}
)

@patch('aprslib.IS')
def test_run(self, mock_is):
from .aprs2mqtt import Aprs2MqttService
config = Config({
'aprs': {
'host': 'localhost',
'port': 1,
'login': 'test'
},
'consumers': [
{ 'filter': 'filter', 'topic': 'topic' }
]
})
svc = Aprs2MqttService(config)
svc.run()
mock_is.assert_called_with('test', host='localhost', port=1)

@patch.multiple('aprslib.IS', connect=DEFAULT,
set_filter=DEFAULT, consumer=DEFAULT)
def test_connect(self, connect, set_filter, consumer):
from .aprs2mqtt import Aprs2MqttService
config = Config({
'aprs': {
'host': 'localhost',
'port': 1,
'login': 'test'
},
'consumers': [
{ 'filter': 'filter', 'topic': 'topic' }
]
})
svc = Aprs2MqttService(config)
svc.run()
connect.assert_called()
set_filter.assert_called_with('filter')
consumer.assert_called()
16 changes: 16 additions & 0 deletions aprs2mqtt/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import os
import unittest
from .config import Config

class TestConfig(unittest.TestCase):
def test_get_config(self):
config = Config({
'aprs': {
'host': 'localhost'
}
})
self.assertEqual(config.get_config('aprs', 'host'), 'localhost')
def test_get_config_env(self):
config = Config({})
os.environ['APRS_HOST'] = 'localhost'
self.assertEqual(config.get_config('aprs', 'host'), 'localhost')
10 changes: 10 additions & 0 deletions aprslib.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{ pkgs ? import <nixpkgs> { } }:
with pkgs.python38Packages; buildPythonPackage {
name = "aprslib";
src = fetchPypi {
pname = "aprslib";
version = "0.6.47";
sha256 = "sha256-V10CX9vpWO5//ilhDfXP5kfLesUf4KdnCnhH1Wuxxgg=";
};
doCheck = false;
}
16 changes: 11 additions & 5 deletions default.nix
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
with import <nixpkgs> {};
pkgs.python37Packages.buildPythonApplication rec {
{ pkgs ? import <nixpkgs> {} }:
let
aprslib = pkgs.callPackage ./aprslib.nix { };
in pkgs.python38Packages.buildPythonPackage rec {
name = "aprs2mqtt";
src = ./.;
propagateBuildInputs = with pkgs.python37Packages; [
paho-mqtt
requests
propagatedBuildInputs = [
(pkgs.python38.withPackages(ps: with ps; [
aprslib
paho-mqtt
plac
pyyaml
]))
];
}
20 changes: 20 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
description = "aprs2mqtt";
outputs = { self, nixpkgs }:
let
pkgs = import nixpkgs {
system = "x86_64-linux";
overlays = [ self.overlay ];
};
in {
overlay = final: prev: {
aprs2mqtt = prev.callPackage ./default.nix { };
};
nixosModules.aprs2mqtt = {
imports = [ ./module.nix ];
nixpkgs.overlays = [
self.overlay
];
};
};
}
Loading

0 comments on commit bc7abb3

Please sign in to comment.