commit 6a7c5e4130e74b91df7ba77ee8166d6a939cd99f Author: Michaela Date: Tue Feb 2 10:33:53 2021 +1000 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5391d87 --- /dev/null +++ b/.gitignore @@ -0,0 +1,138 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..797aa17 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +Simple realtime streaming SDK for sondehub.org V2 API. + +``` +sondehub.Stream(sondes=["serial number"], on_message=callback) +``` +If no `sondes` list is provided then all radiosondes will be streamed. + +On message callback will contain a python dictonary using the [Universal Sonde Telemetry Format](https://github.com/projecthorus/radiosonde_auto_rx/wiki/Suggested-Universal-Sonde-Telemetry-Format) + +Example Usage +-- + +```python +import sondehub + +def on_message(message): + print(message) + +test = sondehub.Stream(sondes=["R3320848"], on_message=on_message) +#test = sondehub.Stream(on_message=on_message) +while 1: + pass + +``` + +CLI Usage +```sh +# all radiosondes +sondehub +# single radiosonde +sondehub --serial "IMET-73217972" +# multiple radiosondes +sondehub --serial "IMET-73217972" --serial "IMET-73217973" +#pipe in jq +sondehub | jq . +{ + "subtype": "SondehubV1", + "temp": "-4.0", + "manufacturer": "SondehubV1", + "serial": "IMET54-55067143", + "lat": "-25.95437", + "frame": "85436", + "datetime": "2021-02-01T23:43:57.043655Z", + "software_name": "SondehubV1", + "humidity": "97.8", + "alt": "5839", + "vel_h": "-9999.0", + "uploader_callsign": "ZS6TVB", + "lon": "28.19082", + "software_version": "SondehubV1", + "type": "SondehubV1", + "time_received": "2021-02-01T23:43:57.043655Z", + "position": "-25.95437,28.19082" +} +.... + +``` \ No newline at end of file diff --git a/example.py b/example.py new file mode 100644 index 0000000..169c572 --- /dev/null +++ b/example.py @@ -0,0 +1,9 @@ +import sondehub + +def on_message(message): + print(message) + +test = sondehub.Stream(sondes=["R3320848"], on_message=on_message) +#test = sondehub.Stream(on_message=on_message) +while 1: + pass diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..3cf58a4 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,20 @@ +[[package]] +name = "paho-mqtt" +version = "1.5.1" +description = "MQTT version 5.0/3.1.1 client class" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +proxy = ["pysocks"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.9" +content-hash = "76d5dfeb617b3f477c15007458ec4f5fad94e4d9ab19f2c8080bb7581685bc1f" + +[metadata.files] +paho-mqtt = [ + {file = "paho-mqtt-1.5.1.tar.gz", hash = "sha256:9feb068e822be7b3a116324e01fb6028eb1d66412bf98595ae72698965cb1cae"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4f4d985 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[tool.poetry] +name = "sondehub" +version = "0.1.0" +description = "" +authors = ["Michaela "] + +[tool.poetry.dependencies] +python = "^3.9" +paho-mqtt = "^1.5.1" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +sondehub = 'sondehub:__main__.main' \ No newline at end of file diff --git a/sondehub/__init__.py b/sondehub/__init__.py new file mode 100644 index 0000000..1ca9efc --- /dev/null +++ b/sondehub/__init__.py @@ -0,0 +1,70 @@ +import paho.mqtt.client as mqtt +from urllib.parse import urlparse +import http.client +import json + +class Stream(): + def __init__(self, sondes: list = ["#"], on_connect=None, on_message=None): + self.mqttc = mqtt.Client(transport="websockets") + self._sondes = sondes + self.ws_connect() + self.on_connect = on_connect + self.on_message = on_message + + def add_sonde(self, sonde): + if sonde not in self._sondes: + self._sondes.append(sonde) + (result, mid) = self.mqttc.subscribe(f"sondes/{sonde}", 0) + if result != mqtt.MQTT_ERR_SUCCESS: + self.ws_connect() + def remove_sonde(self, sonde): + self._sondes.remove(sonde) + (result, mid) = self.mqttc.unsubscribe(f"sondes/{sonde}", 0) + if result != mqtt.MQTT_ERR_SUCCESS: + self.ws_connect() + + def ws_connect(self): + url = self.get_url() + urlparts = urlparse(url) + headers = { + "Host": "{0:s}".format(urlparts.netloc), + } + + self.mqttc.on_message = self._on_message # self.on_message + self.mqttc.on_connect = self._on_connect + self.mqttc.on_disconnect = self._on_disconnect + + self.mqttc.ws_set_options(path="{}?{}".format(urlparts.path, urlparts.query), headers=headers) + try: + self.mqttc.tls_set() + except ValueError: + pass + self.mqttc.connect(urlparts.netloc, 443, 60) + self.mqttc.loop_start() + for sonde in self._sondes: + self.add_sonde(sonde) + + def get_url(self): + conn = http.client.HTTPSConnection("api.v2.sondehub.org") + conn.request("GET", "/sondes/websocket") + res = conn.getresponse() + data = res.read() + return data.decode("utf-8") + + def _on_message(self, mqttc, obj, msg): + if self.on_message: + self.on_message(json.loads(msg.payload)) + def _on_connect(self, mqttc, obj, flags, rc): + if mqtt.MQTT_ERR_SUCCESS != rc: + self.ws_connect() + if self.on_connect: + self.on_connect() + + def _on_disconnect(self, client, userdata, rc): + self.ws_connect() + + def __exit__(self, type, value, traceback): + self.mqttc.disconnect() + + def disconnect(self): + self.mqttc.disconnect() diff --git a/sondehub/__main__.py b/sondehub/__main__.py new file mode 100644 index 0000000..237af0a --- /dev/null +++ b/sondehub/__main__.py @@ -0,0 +1,19 @@ +import json +import sondehub +import argparse + +def on_message(message): + print(json.dumps(message)) + +def main(): + parser = argparse.ArgumentParser(description='Sondehub CLI') + parser.add_argument('--serial', dest="sondes", default=["#"],nargs="*", help="Filter to sonde serial", type=str, action="extend") + args = parser.parse_args() + if len(args.sondes) > 1: # we need to drop the default value if the user specifies sepcific sondes + args.sondes = args.sondes[1:] + test = sondehub.Stream(on_message=on_message, sondes=args.sondes) + while 1: + pass + +if __name__ == "__main__": + main() \ No newline at end of file