├── .dockerignore ├── .github ├── docs-static │ ├── CNAME │ └── index.html └── workflows │ ├── gh-pages-static.yml │ ├── release-docker-full.yml │ ├── release-docker-standard.yml │ └── tests.yml ├── .gitignore ├── .readthedocs.yml ├── .vscode ├── launch.json └── settings.json ├── CHANGES.rst ├── CITATION.cff ├── Dockerfile ├── Dockerfile.full ├── Dockerfile.mqttwarn-slack ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── assets ├── apns.png ├── desktopnotify.jpg ├── google-definition.jpg ├── gss.png ├── hipchat.png ├── icinga.jpg ├── ionic.png ├── irccat.png ├── linuxnotify.png ├── logo │ ├── img │ │ ├── hz_wordmark_blk_200x75.png │ │ ├── hz_wordmark_blk_500x187.png │ │ ├── hz_wordmark_wht_200x75.png │ │ ├── hz_wordmark_wht_500x187.png │ │ ├── markonly_200x200.png │ │ ├── markonly_500x500.png │ │ ├── markword_200x200.png │ │ └── markword_500x500.png │ └── src │ │ ├── hz_wordmark_blk.svg │ │ ├── hz_wordmark_wht.svg │ │ ├── markonly.svg │ │ └── wordmark.svg ├── mattermost.png ├── mqttwarn.png ├── pastebin.png ├── prowl.jpg ├── pushalot.png ├── pushbullet.jpg ├── pushover.png ├── pushsafer.jpg ├── slack.png ├── telegram.png ├── tootpaste.png ├── twilio.jpg ├── twitter.jpg └── zabbix.png ├── codecov.yml ├── contrib ├── amqp-puka-get.py └── zabbix_mqtt_agent.py ├── docker-compose.yml ├── docs ├── .gitignore ├── Makefile ├── assets ├── conf.py ├── configure │ ├── index.rst │ ├── mqttwarn.ini.md │ ├── service.md │ ├── task.md │ ├── topic.md │ └── transformation.md ├── examples ├── index.rst ├── make.bat ├── mqttwarn-logo.png ├── notifier-catalog.md ├── readme.rst ├── requirements.txt ├── usage │ ├── freebsd.md │ ├── index.rst │ ├── oci.md │ ├── pip.md │ └── standalone.md └── workbench │ ├── backlog.rst │ ├── changelog.rst │ └── sandbox.rst ├── etc ├── OpenWRT.init ├── mqttwarn.default ├── mqttwarn.init ├── mqttwarn.logrotate ├── mqttwarn.openrc ├── mqttwarn.service ├── supervisor.ini └── zabbix-template.xml ├── examples ├── __init__.py ├── alexa │ ├── alexa.ini │ ├── announce_stdin │ ├── readme.md │ ├── saystdin │ └── secrets.sh ├── arduino-temperature │ └── readme.md ├── conftest.py ├── frigate │ ├── .env │ ├── .gitignore │ ├── README.rst │ ├── assets │ │ ├── frigate-event-end.json │ │ ├── frigate-event-false-positive.json │ │ ├── frigate-event-full.json │ │ ├── frigate-event-new-good.json │ │ ├── frigate-event-new-ignored.json │ │ ├── frigate-event-update-good.json │ │ ├── frigate-event-update-samezone.json │ │ └── frigate-event-update-stationary.json │ ├── docker-compose.yml │ ├── frigate.ini │ ├── frigate.py │ ├── publish.sh │ └── test_frigate.py ├── hiveeyes │ ├── __init__.py │ ├── hiveeyes.ini │ └── hiveeyes.py ├── homie │ ├── __init__.py │ ├── homie.ini │ └── homie.py ├── mediaplayer │ ├── mqttwarn-mplayer.ini │ └── readme.md ├── owntracks-ntfy │ ├── mqttwarn-owntracks.ini │ ├── mqttwarn-owntracks.py │ └── readme.md ├── readme.md ├── warntoggle │ ├── README.rst │ ├── mqttwarn │ │ └── customfunctions.py │ └── www │ │ ├── warntoggle.json │ │ └── warntoggle.py └── zabbix-iot │ ├── README.md │ ├── mqttwarn-zabbix-iot.ini │ ├── mqttwarn-zabbix-iot.py │ └── readme.md ├── mqttwarn ├── __init__.py ├── __main__.py ├── commands.py ├── configuration.py ├── context.py ├── core.py ├── cron.py ├── examples │ ├── __init__.py │ └── basic │ │ ├── mqttwarn.ini │ │ └── udf.py ├── model.py ├── services │ ├── README.md │ ├── __init__.py │ ├── alexa-notify-me.py │ ├── amqp.py │ ├── apns.py │ ├── apprise.py │ ├── apprise_multi.py │ ├── apprise_single.py │ ├── apprise_util.py │ ├── asterisk.py │ ├── autoremote.py │ ├── azure_iot.py │ ├── carbon.py │ ├── celery.py │ ├── chromecast.py │ ├── dbus.py │ ├── desktopnotify.py │ ├── dnsupdate.py │ ├── emoncms.py │ ├── execute.py │ ├── fbchat.py │ ├── file.py │ ├── freeswitch.py │ ├── gss2.py │ ├── hangbot.py │ ├── http_urllib.py │ ├── icinga2.py │ ├── ifttt.py │ ├── influxdb.py │ ├── ionic.py │ ├── irccat.py │ ├── linuxnotify.py │ ├── log.py │ ├── mattermost.py │ ├── mqtt.py │ ├── mqtt_filter.py │ ├── mqttpub.py │ ├── mysql.py │ ├── mysql_dynamic.py │ ├── mysql_remap.py │ ├── mythtv.py │ ├── nntp.py │ ├── noop.py │ ├── nsca.py │ ├── ntfy.py │ ├── osxsay.py │ ├── pastebinpub.py │ ├── pipe.py │ ├── postgres.py │ ├── prowl.py │ ├── pushbullet.py │ ├── pushover.py │ ├── pushsafer.py │ ├── redispub.py │ ├── rrdtool.py │ ├── serial.py │ ├── slack.py │ ├── slixmpp.py │ ├── smtp.py │ ├── sqlite.py │ ├── sqlite_json2cols.py │ ├── sqlite_timestamp.py │ ├── ssh.py │ ├── syslog.py │ ├── telegram.py │ ├── thingspeak.py │ ├── tootpaste.py │ ├── twilio.py │ ├── twitter.py │ ├── websocket.py │ ├── xbmc.py │ ├── xmpp.py │ └── zabbix.py ├── testing │ ├── __init__.py │ └── fixtures.py ├── util.py └── vendor │ ├── ZabbixSender.py │ └── __init__.py ├── pyproject.toml ├── requirements-release.txt ├── setup.py ├── templates ├── demo.j2 ├── hiveeyes-alert.j2 └── test.jinja ├── tests ├── __init__.py ├── acme │ ├── __init__.py │ └── foobar.py ├── conftest.py ├── etc │ ├── __init__.py │ ├── better-addresses.ini │ ├── empty-functions.ini │ ├── full.ini │ ├── functions_bad.py │ ├── functions_good.py │ ├── logging-levels.ini │ ├── no-functions.ini │ ├── password.txt │ ├── service-loading.ini │ └── with-variables.ini ├── fixtures │ ├── __init__.py │ └── ntfy.py ├── services │ ├── __init__.py │ ├── pushsafer │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── test_pushsafer_common.py │ │ ├── test_pushsafer_v1.py │ │ ├── test_pushsafer_v2.py │ │ └── util.py │ ├── test_alexa.py │ ├── test_amqp.py │ ├── test_apns.py │ ├── test_apprise_multi.py │ ├── test_apprise_ntfy.py │ ├── test_apprise_single.py │ ├── test_asterisk.py │ ├── test_autoremote.py │ ├── test_azure.py │ ├── test_carbon.py │ ├── test_desktopnotify.py │ ├── test_execute.py │ ├── test_file.py │ ├── test_http.py │ ├── test_irccat.py │ ├── test_log.py │ ├── test_noop.py │ ├── test_ntfy.py │ ├── test_ntfy_integration.py │ ├── test_pushbullet.py │ ├── test_pushover.py │ └── test_smtp.py ├── test_commands.py ├── test_configuration.py ├── test_context.py ├── test_core_infra.py ├── test_core_job.py ├── test_core_main.py ├── test_cron.py ├── test_e2e.py ├── test_model.py ├── test_util.py └── util.py └── tox.ini /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !.git 3 | !etc 4 | !mqttwarn 5 | !tests 6 | !pyproject.toml 7 | !setup.py 8 | !MANIFEST.in 9 | !*.ini 10 | !*.md 11 | !*.rst 12 | -------------------------------------------------------------------------------- /.github/docs-static/CNAME: -------------------------------------------------------------------------------- 1 | mqttwarn.readthedocs.io -------------------------------------------------------------------------------- /.github/docs-static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Redirecting to https://mqttwarn.readthedocs.io/ 4 | 5 | 6 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages-static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v3 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v3 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v1 38 | with: 39 | path: '.github/docs-static' 40 | - name: Deploy to GitHub Pages 41 | id: deployment 42 | uses: actions/deploy-pages@v2 43 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | # Allow job to be triggered manually. 10 | workflow_dispatch: 11 | 12 | # Cancel in-progress jobs when pushing to the same branch. 13 | concurrency: 14 | cancel-in-progress: true 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | 17 | jobs: 18 | tests: 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | os: ["ubuntu-20.04"] 24 | python-version: ["3.6", "3.7", "3.12"] 25 | include: 26 | - os: "macos-latest" 27 | python-version: "3.12" 28 | - os: "windows-latest" 29 | python-version: "3.11" 30 | env: 31 | OS: ${{ matrix.os }} 32 | PYTHON: ${{ matrix.python-version }} 33 | 34 | name: Python ${{ matrix.python-version }} on OS ${{ matrix.os }} 35 | steps: 36 | 37 | - name: Acquire sources 38 | uses: actions/checkout@v3 39 | 40 | # https://github.com/docker-practice/actions-setup-docker 41 | # - name: Install Docker 42 | # if: runner.os == 'Linux' 43 | # uses: docker-practice/actions-setup-docker@master 44 | 45 | - name: Display Docker version 46 | if: runner.os == 'Linux' 47 | run: | 48 | docker version 49 | 50 | - name: Setup Python 51 | uses: actions/setup-python@v4 52 | with: 53 | python-version: ${{ matrix.python-version }} 54 | architecture: x64 55 | cache: 'pip' 56 | cache-dependency-path: 'setup.py' 57 | 58 | - name: Setup project 59 | run: | 60 | # Install package in editable mode. 61 | pip install versioningit wheel 62 | pip install --editable=.[test,develop] 63 | 64 | - name: Check code style 65 | if: matrix.python-version != '3.6' 66 | run: | 67 | poe lint 68 | 69 | - name: Run tests 70 | run: | 71 | poe test 72 | 73 | - name: Upload coverage to Codecov 74 | uses: codecov/codecov-action@v3 75 | with: 76 | files: ./coverage.xml 77 | flags: unittests 78 | env_vars: OS,PYTHON 79 | name: codecov-umbrella 80 | fail_ci_if_error: false 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.idea 3 | /.venv* 4 | /dist* 5 | /docs/_build 6 | /build 7 | *.pyc 8 | *.egg-info 9 | *.sln 10 | *.suo 11 | *.pyproj 12 | services/winscp.rnd 13 | .vs/slnx.sqlite 14 | .vs/VSWorkspaceState.json 15 | .vs/ProjectSettings.json 16 | udf.py 17 | .pytest_cache 18 | .pytest_results 19 | .coverage* 20 | coverage.xml 21 | .tox 22 | .ruff_cache 23 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | 4 | # Details 5 | # - https://docs.readthedocs.io/en/stable/config-file/v2.html 6 | 7 | # Required 8 | version: 2 9 | 10 | build: 11 | os: "ubuntu-22.04" 12 | tools: 13 | python: "3.11" 14 | 15 | # Build documentation in the docs/ directory with Sphinx 16 | sphinx: 17 | configuration: docs/conf.py 18 | 19 | python: 20 | install: 21 | - requirements: docs/requirements.txt 22 | 23 | # Optionally build your docs in additional formats such as PDF 24 | # formats: 25 | # - pdf 26 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // VSCode launch configuration settings file for mqttwarn. 3 | 4 | // Use IntelliSense to learn about possible attributes. 5 | // Hover to view descriptions of existing attributes. 6 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 7 | "version": "0.2.0", 8 | "configurations": [ 9 | 10 | { 11 | "name": "Launch", 12 | "type": "python", 13 | "request": "launch", 14 | "program": "mqttwarn" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "${workspaceFolder}/.venv/bin/python", 3 | "python.testing.pytestEnabled": true, 4 | "python.testing.pytestPath": "${workspaceFolder}/.venv/bin/pytest" 5 | } -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | --- 2 | cff-version: 1.2.0 3 | message: If you use this software, please cite it using these metadata. 4 | 5 | title: mqttwarn 6 | url: https://mqttwarn.readthedocs.org/ 7 | abstract: mqttwarn - subscribe to MQTT topics and notify pluggable services 8 | authors: 9 | - name: Jan-Piet Mens 10 | email: jpmens@gmail.com 11 | - name: Ben Jones 12 | email: ben.jones12@gmail.com 13 | - name: Andreas Motl 14 | email: andreas.motl@panodata.org 15 | 16 | date-published: 2014-02-09 17 | date-released: 2023-04-13 18 | type: software 19 | license: EPL 20 | license-url: https://github.com/mqtt-tools/mqttwarn/blob/main/LICENSE 21 | repository-code: https://github.com/mqtt-tools/mqttwarn 22 | keywords: 23 | - acquisition 24 | - data 25 | - engine 26 | - notification 27 | - plugins 28 | - push 29 | - transformation 30 | - mosquitto 31 | - mqtt 32 | - mqttwarn 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Docker build file for `mqttwarn-standard`. 2 | # 3 | # Invoke like: 4 | # 5 | # docker build --tag=local/mqttwarn-standard --file=Dockerfile . 6 | # 7 | FROM python:3.11-slim-bullseye 8 | 9 | 10 | # ===== 11 | # Build 12 | # ===== 13 | 14 | # Install build prerequisites. 15 | RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache 16 | RUN \ 17 | --mount=type=cache,id=apt,sharing=locked,target=/var/cache/apt \ 18 | --mount=type=cache,id=apt,sharing=locked,target=/var/lib/apt \ 19 | true \ 20 | && apt-get update \ 21 | && apt-get install --no-install-recommends --no-install-suggests --yes git 22 | 23 | # Create /etc/mqttwarn 24 | RUN mkdir -p /etc/mqttwarn 25 | WORKDIR /etc/mqttwarn 26 | 27 | # Add user "mqttwarn" 28 | RUN groupadd -r mqttwarn && useradd -r -g mqttwarn mqttwarn 29 | RUN chown -R mqttwarn:mqttwarn /etc/mqttwarn 30 | 31 | # Install package. 32 | COPY . /src 33 | RUN --mount=type=cache,id=pip,target=/root/.cache/pip \ 34 | true \ 35 | && pip install --upgrade pip \ 36 | && pip install --prefer-binary versioningit wheel \ 37 | && pip install --use-pep517 --prefer-binary '/src' 38 | 39 | # Uninstall build prerequisites again. 40 | RUN apt-get --yes remove --purge git && apt-get --yes autoremove 41 | 42 | # Purge /src and /tmp directories. 43 | RUN rm -rf /src /tmp/* 44 | 45 | 46 | # ======= 47 | # Runtime 48 | # ======= 49 | 50 | # Make process run as "mqttwarn" user 51 | USER mqttwarn 52 | 53 | # Use configuration file from host 54 | VOLUME ["/etc/mqttwarn"] 55 | 56 | # Set default configuration path 57 | ENV MQTTWARNINI="/etc/mqttwarn/mqttwarn.ini" 58 | 59 | # Invoke program 60 | CMD mqttwarn 61 | -------------------------------------------------------------------------------- /Dockerfile.full: -------------------------------------------------------------------------------- 1 | # Docker build file for `mqttwarn-full`. 2 | # 3 | # Invoke like: 4 | # 5 | # docker build --tag=local/mqttwarn-full --file=Dockerfile.full . 6 | # 7 | FROM python:3.11-slim-bullseye 8 | 9 | 10 | # ===== 11 | # Build 12 | # ===== 13 | 14 | # Install build prerequisites. 15 | RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache 16 | RUN \ 17 | --mount=type=cache,id=apt,sharing=locked,target=/var/cache/apt \ 18 | --mount=type=cache,id=apt,sharing=locked,target=/var/lib/apt \ 19 | true \ 20 | && apt-get update \ 21 | && apt-get install --no-install-recommends --no-install-suggests --yes git build-essential libmariadb-dev pkg-config 22 | 23 | # Create /etc/mqttwarn 24 | RUN mkdir -p /etc/mqttwarn 25 | WORKDIR /etc/mqttwarn 26 | 27 | # Add user "mqttwarn" 28 | RUN groupadd -r mqttwarn && useradd -r -g mqttwarn mqttwarn 29 | RUN chown -R mqttwarn:mqttwarn /etc/mqttwarn 30 | 31 | # Install package. 32 | COPY . /src 33 | RUN --mount=type=cache,id=pip,target=/root/.cache/pip \ 34 | true \ 35 | && pip install --upgrade pip \ 36 | && pip install --prefer-binary versioningit wheel \ 37 | && pip install --use-pep517 --prefer-binary '/src[all]' 38 | 39 | # Uninstall build prerequisites again. 40 | RUN apt-get --yes remove --purge git build-essential libmariadb-dev pkg-config && apt-get --yes autoremove 41 | 42 | # Purge /src and /tmp directories. 43 | RUN rm -rf /src /tmp/* 44 | 45 | 46 | # ======= 47 | # Runtime 48 | # ======= 49 | 50 | # Make process run as "mqttwarn" user 51 | USER mqttwarn 52 | 53 | # Use configuration file from host 54 | VOLUME ["/etc/mqttwarn"] 55 | 56 | # Set default configuration path 57 | ENV MQTTWARNINI="/etc/mqttwarn/mqttwarn.ini" 58 | 59 | # Invoke program 60 | CMD mqttwarn 61 | -------------------------------------------------------------------------------- /Dockerfile.mqttwarn-slack: -------------------------------------------------------------------------------- 1 | # Docker build file for mqttwarn, with Slack SDK. 2 | # 3 | # Invoke like: 4 | # 5 | # docker build --tag=mqttwarn-slack --file=Dockerfile.mqttwarn-slack . 6 | # 7 | 8 | # Derive from upstream image. 9 | FROM ghcr.io/mqtt-tools/mqttwarn-standard:latest 10 | 11 | # Make package installation run as "root" user 12 | USER root 13 | 14 | # Install Slack SDK. 15 | RUN pip install wheel 16 | RUN pip install mqttwarn[slack] 17 | 18 | # Make process run as "mqttwarn" user 19 | USER mqttwarn 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include setup.cfg *.txt *.rst *.md 2 | exclude .bumpversion.cfg 3 | recursive-include mqttwarn *.ini *.py 4 | prune examples 5 | prune vendor 6 | prune templates 7 | prune tests 8 | 9 | # Documentation 10 | recursive-include docs * 11 | prune docs/_build 12 | global-exclude */__pycache__/* 13 | global-exclude *.pyc 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # ============ 2 | # Main targets 3 | # ============ 4 | 5 | 6 | # ------------- 7 | # Configuration 8 | # ------------- 9 | 10 | $(eval venvpath := .venv) 11 | $(eval pip := $(venvpath)/bin/pip) 12 | $(eval python := $(venvpath)/bin/python) 13 | $(eval pytest := $(venvpath)/bin/pytest) 14 | $(eval twine := $(venvpath)/bin/twine) 15 | $(eval sphinx := $(venvpath)/bin/sphinx-build) 16 | $(eval sphinx-autobuild := $(venvpath)/bin/sphinx-autobuild) 17 | $(eval isort := $(venvpath)/bin/isort) 18 | $(eval black := $(venvpath)/bin/black) 19 | $(eval poe := $(venvpath)/bin/poe) 20 | 21 | # Setup Python virtualenv 22 | setup-virtualenv: 23 | @test -e $(python) || python3 -m venv $(venvpath) 24 | $(pip) install versioningit 25 | 26 | 27 | # ------- 28 | # Testing 29 | # ------- 30 | 31 | # Run the main test suite 32 | test: install-tests 33 | @$(poe) test 34 | 35 | test-refresh: install-tests test 36 | 37 | test-junit: install-tests 38 | @$(pytest) -vvv tests --junit-xml .pytest_results/pytest.xml 39 | 40 | test-coverage: install-tests 41 | @$(pytest) --cov-report html:.pytest_results/htmlcov 42 | 43 | 44 | # ---------------------- 45 | # Linting and Formatting 46 | # ---------------------- 47 | format: install-tests 48 | $(poe) format 49 | 50 | 51 | # ------- 52 | # Release 53 | # ------- 54 | 55 | # Release this piece of software 56 | release: 57 | poe release 58 | 59 | 60 | # ------------- 61 | # Documentation 62 | # ------------- 63 | 64 | # Build the documentation 65 | docs-html: install-doctools 66 | cd docs; make html 67 | 68 | docs-serve: 69 | cd docs/_build/html; python3 -m http.server 70 | 71 | docs-autobuild: install-doctools 72 | $(sphinx-autobuild) --open-browser docs docs/_build 73 | 74 | 75 | # =============== 76 | # Utility targets 77 | # =============== 78 | install-doctools: 79 | @test -e $(python) || python3 -m venv $(venvpath) 80 | $(pip) install --quiet --upgrade --requirement=docs/requirements.txt 81 | 82 | install-tests: setup-virtualenv 83 | @test -e $(pytest) || $(pip) install --editable=.[test,develop] --upgrade 84 | @touch $(venvpath)/bin/activate 85 | @mkdir -p .pytest_results 86 | -------------------------------------------------------------------------------- /assets/apns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/apns.png -------------------------------------------------------------------------------- /assets/desktopnotify.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/desktopnotify.jpg -------------------------------------------------------------------------------- /assets/google-definition.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/google-definition.jpg -------------------------------------------------------------------------------- /assets/gss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/gss.png -------------------------------------------------------------------------------- /assets/hipchat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/hipchat.png -------------------------------------------------------------------------------- /assets/icinga.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/icinga.jpg -------------------------------------------------------------------------------- /assets/ionic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/ionic.png -------------------------------------------------------------------------------- /assets/irccat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/irccat.png -------------------------------------------------------------------------------- /assets/linuxnotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/linuxnotify.png -------------------------------------------------------------------------------- /assets/logo/img/hz_wordmark_blk_200x75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/logo/img/hz_wordmark_blk_200x75.png -------------------------------------------------------------------------------- /assets/logo/img/hz_wordmark_blk_500x187.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/logo/img/hz_wordmark_blk_500x187.png -------------------------------------------------------------------------------- /assets/logo/img/hz_wordmark_wht_200x75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/logo/img/hz_wordmark_wht_200x75.png -------------------------------------------------------------------------------- /assets/logo/img/hz_wordmark_wht_500x187.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/logo/img/hz_wordmark_wht_500x187.png -------------------------------------------------------------------------------- /assets/logo/img/markonly_200x200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/logo/img/markonly_200x200.png -------------------------------------------------------------------------------- /assets/logo/img/markonly_500x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/logo/img/markonly_500x500.png -------------------------------------------------------------------------------- /assets/logo/img/markword_200x200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/logo/img/markword_200x200.png -------------------------------------------------------------------------------- /assets/logo/img/markword_500x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/logo/img/markword_500x500.png -------------------------------------------------------------------------------- /assets/mattermost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/mattermost.png -------------------------------------------------------------------------------- /assets/mqttwarn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/mqttwarn.png -------------------------------------------------------------------------------- /assets/pastebin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/pastebin.png -------------------------------------------------------------------------------- /assets/prowl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/prowl.jpg -------------------------------------------------------------------------------- /assets/pushalot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/pushalot.png -------------------------------------------------------------------------------- /assets/pushbullet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/pushbullet.jpg -------------------------------------------------------------------------------- /assets/pushover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/pushover.png -------------------------------------------------------------------------------- /assets/pushsafer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/pushsafer.jpg -------------------------------------------------------------------------------- /assets/slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/slack.png -------------------------------------------------------------------------------- /assets/telegram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/telegram.png -------------------------------------------------------------------------------- /assets/tootpaste.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/tootpaste.png -------------------------------------------------------------------------------- /assets/twilio.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/twilio.jpg -------------------------------------------------------------------------------- /assets/twitter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/twitter.jpg -------------------------------------------------------------------------------- /assets/zabbix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/assets/zabbix.png -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # https://docs.codecov.io/docs/common-recipe-list 2 | # https://docs.codecov.io/docs/commit-status#patch-status 3 | 4 | coverage: 5 | status: 6 | project: 7 | default: 8 | target: auto # the required coverage value 9 | threshold: 4% # the leniency in hitting the target 10 | patch: 11 | default: 12 | target: 0% 13 | informational: true 14 | -------------------------------------------------------------------------------- /contrib/amqp-puka-get.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import print_function 4 | import sys 5 | import puka 6 | import json 7 | 8 | exchange = 'mqttwarn' 9 | routing_key = 'all' 10 | 11 | client = puka.Client('amqp://guest:guest@localhost') 12 | promise = client.connect() 13 | client.wait(promise) 14 | 15 | promise = client.exchange_declare(exchange=exchange, type='direct') 16 | client.wait(promise) 17 | 18 | promise = client.queue_declare(exclusive=True) 19 | queue_name = client.wait(promise)['queue'] 20 | 21 | promise = client.queue_bind(exchange=exchange, queue=queue_name, routing_key=routing_key) 22 | client.wait(promise) 23 | 24 | consume = client.basic_consume(queue=queue_name, no_ack=False) 25 | while True: 26 | try: 27 | msg = client.wait(consume) 28 | print(json.dumps(msg, indent=4)) 29 | client.basic_ack(msg) 30 | except KeyboardInterrupt: 31 | client.close() 32 | sys.exit(0) 33 | 34 | -------------------------------------------------------------------------------- /contrib/zabbix_mqtt_agent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Example "agent" for Zabbix/mqttwarn which publishes two metrics 5 | # every few seconds. 6 | 7 | import paho.mqtt.client as paho # pip install paho-mqtt 8 | import ssl 9 | import time 10 | import sys 11 | from random import randint 12 | 13 | __author__ = 'Jan-Piet Mens ' 14 | __copyright__ = 'Copyright 2014 Jan-Piet Mens' 15 | __license__ = """Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)""" 16 | 17 | CLIENT = 'jog09' 18 | HOST_TOPIC = "zabbix/clients/%s" % CLIENT 19 | 20 | mqttc = paho.Client(clean_session=True, userdata=None) 21 | 22 | def metric(name, value): 23 | mqttc.publish("zabbix/item/%s/%s" % (CLIENT, name), value) 24 | mqttc.loop() 25 | 26 | mqttc.tls_set('/Users/jpm/tmp/mqtt/root.ca', 27 | tls_version=ssl.PROTOCOL_TLSv1) 28 | 29 | mqttc.tls_insecure_set(True) # Ensure False in production 30 | 31 | # If this client dies, ensure broker publishes our death on our behalf (LWT) 32 | mqttc.will_set(HOST_TOPIC, payload="0", qos=0, retain=True) 33 | 34 | # mqttc.username_pw_set('john', 'secret') 35 | mqttc.connect("localhost", 8883, 60) 36 | 37 | # Indicate host is up 38 | mqttc.publish(HOST_TOPIC, "1") 39 | rc = 0 40 | while rc == 0: 41 | try: 42 | rc = mqttc.loop() 43 | 44 | metric('system.cpu.load', randint(2, 8)) 45 | metric('time.stamp', time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())) 46 | 47 | time.sleep(10) 48 | except KeyboardInterrupt: 49 | sys.exit(0) 50 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # To use this Docker Compose file for mqttwarn: 2 | # 3 | # 1. Acquire "docker-compose.yml": 4 | # wget https://raw.githubusercontent.com/mqtt-tools/mqttwarn/main/docker-compose.yml 5 | # 6 | # 2. Create a directory for mqttwarn to store "mqttwarn.ini" and "funcs.py" in: 7 | # mkdir /path/to/mqttwarn 8 | # 9 | # 3. Acquire "mqttwarn.ini": 10 | # wget https://raw.githubusercontent.com/mqtt-tools/mqttwarn/main/mqttwarn/examples/basic/mqttwarn.ini -O /path/to/mqtwarn/mqttwarn.ini 11 | # 12 | # 4. Define the location to the custom functions file within "mqttwarn.ini": 13 | # 14 | # functions = 'funcs.py' 15 | version: '3' 16 | 17 | services: 18 | mqttwarn: 19 | image: ghcr.io/mqtt-tools/mqttwarn-full:latest 20 | container_name: mqttwarn 21 | restart: always 22 | volumes: 23 | - /path/to/mqttwarn:/etc/mqttwarn 24 | - /etc/localtime:/etc/localtime:ro 25 | # If you want to change the default location of mqttwarn.ini, uncomment the following lines: 26 | #environment: 27 | # - MQTTWARNINI=/etc/mqttwarn/mqttwarn.ini 28 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/assets: -------------------------------------------------------------------------------- 1 | ../assets -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = "mqttwarn" 10 | copyright = "2014-2023, Jan-Piet Mens, Ben Jones, Andreas Motl" 11 | author = "Jan-Piet Mens, Ben Jones, Andreas Motl" 12 | 13 | # -- General configuration --------------------------------------------------- 14 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 15 | 16 | extensions = [ 17 | "myst_parser", 18 | "sphinx_copybutton", 19 | "sphinx_togglebutton", 20 | "sphinx.ext.intersphinx", 21 | "sphinx.ext.todo", 22 | "sphinx.ext.ifconfig", 23 | "sphinxext.opengraph", 24 | ] 25 | 26 | 27 | templates_path = ["_templates"] 28 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 29 | 30 | 31 | # -- Options for HTML output ------------------------------------------------- 32 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 33 | 34 | html_theme = "furo" 35 | html_static_path = ["_static"] 36 | html_logo = "mqttwarn-logo.png" 37 | html_show_sourcelink = True 38 | 39 | 40 | # == Extension configuration ========================================== 41 | 42 | todo_include_todos = True 43 | intersphinx_mapping = { 44 | "python": ("https://docs.python.org/3", None), 45 | "sphinx": ("https://www.sphinx-doc.org/en/master/", None), 46 | "myst": ("https://myst-parser.readthedocs.io/en/latest", None), 47 | } 48 | linkcheck_ignore = [r"https://www.researchgate.net/publication/.*"] 49 | sphinx_tabs_valid_builders = ["linkcheck"] 50 | 51 | 52 | # -- Options for MyST ------------------------------------------------- 53 | myst_heading_anchors = 3 54 | myst_enable_extensions = [ 55 | "attrs_block", 56 | "attrs_inline", 57 | "colon_fence", 58 | "deflist", 59 | "fieldlist", 60 | "linkify", 61 | "strikethrough", 62 | "tasklist", 63 | ] 64 | 65 | # -- Options for sphinx-copybutton ------------------------------------ 66 | copybutton_remove_prompts = True 67 | copybutton_line_continuation_character = "\\" 68 | copybutton_prompt_text = r">>> |\.\.\. |\$ |sh\$ |PS> |cr> |mysql> |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: " 69 | copybutton_prompt_is_regexp = True 70 | 71 | # -- Options for sphinxext-opengraph ---------------------------------- 72 | ogp_site_url = "https://mqttwarn.readthedocs.io/" 73 | ogp_image = "https://mqttwarn.readthedocs.io/en/latest/_static/mqttwarn-logo.png" 74 | ogp_description_length = 300 75 | ogp_enable_meta_description = True 76 | -------------------------------------------------------------------------------- /docs/configure/index.rst: -------------------------------------------------------------------------------- 1 | .. _configure: 2 | 3 | ############# 4 | Configuration 5 | ############# 6 | 7 | This part of the documentation covers the configuration of mqttwarn. The second 8 | step to using any software package after its :ref:`installation `, 9 | is getting it properly configured. Please read this section carefully. 10 | 11 | To directly jump to the corresponding sections, visit 12 | :ref:`mqttwarn.ini`, :ref:`services`, :ref:`topics`, and :ref:`transformations`. 13 | 14 | 15 | ****************************** 16 | Application configuration file 17 | ****************************** 18 | 19 | In this section, you will learn about the layout, structure, and semantics of 20 | the application configuration file ``mqttwarn.ini``. 21 | mqttwarn needs it properly configured in order to operate successfully. 22 | 23 | 24 | Create starter files 25 | ==================== 26 | 27 | 28 | Blueprints for mqttwarn configuration files are available within the mqttwarn 29 | repository at `mqttwarn.ini`_ and `udf.py`_. You can use them as personal 30 | starter files, and edit them to your taste:: 31 | 32 | # Create configuration file. 33 | mqttwarn make-config > mqttwarn.ini 34 | 35 | # Create file hosting user-defined functions. 36 | mqttwarn make-udf > udf.py 37 | 38 | .. important:: 39 | 40 | If you are using PowerShell on Windows 10, you may find the files to be written 41 | using the ``UTF-16`` charset encoding. However, ``mqttwarn`` works with ``UTF-8``. 42 | In order to switch to ``UTF-8``, please invoke this command beforehand. 43 | 44 | .. code-block:: powershell 45 | 46 | $PSDefaultParameterValues['Out-File:Encoding'] = 'utf8' 47 | 48 | 49 | Learn and edit configuration 50 | ============================ 51 | 52 | In order to implement mqttwarn for your use case, you will need to edit its 53 | configuration files according to your needs. 54 | 55 | To learn about the application configuration file ``mqttwarn.ini``, please 56 | follow up reading the following sections of the documentation. 57 | 58 | .. toctree:: 59 | :maxdepth: 1 60 | 61 | mqttwarn.ini 62 | service 63 | topic 64 | transformation 65 | task 66 | 67 | Notification "services" define where messages are routed to, "topics" are 68 | definitions of MQTT topic subscriptions, and with "transformations", you are 69 | defining how messages will be filtered, decoded, and re-formatted while 70 | mqttwarn is processing them. 71 | 72 | 73 | .. _mqttwarn.ini: https://github.com/mqtt-tools/mqttwarn/blob/main/mqttwarn/examples/basic/mqttwarn.ini 74 | .. _udf.py: https://github.com/mqtt-tools/mqttwarn/blob/main/mqttwarn/examples/basic/udf.py 75 | -------------------------------------------------------------------------------- /docs/configure/task.md: -------------------------------------------------------------------------------- 1 | (task)= 2 | (tasks)= 3 | # Tasks 4 | 5 | 6 | ## Periodic tasks 7 | 8 | _mqttwarn_ can use functions you define in the file specified `[defaults]` section 9 | to periodically do whatever you want, for example, publish an MQTT message. There 10 | are two things you have to do: 11 | 12 | 1. Create the function 13 | 2. Configure _mqttwarn_ to use that function and specify the interval in seconds 14 | 15 | Assume we have the following user-defined function. 16 | ```python 17 | from mqttwarn.model import Service 18 | 19 | def pinger(srv: Service): 20 | srv.mqttc.publish("pt/PINGER", "Hello from mqttwarn!", qos=0) 21 | ``` 22 | 23 | We configure this function to run every, say, 10 seconds, in the `mqttwarn.ini`, 24 | in the `[cron]` section: 25 | 26 | ```ini 27 | [cron] 28 | pinger = 10.5 29 | ``` 30 | 31 | Each keyword in the `[cron]` section specifies the name of one of your custom 32 | functions, and its float value is an interval in _seconds_ after which your 33 | user-defined function, in this case `pinger()`, is invoked. Your function has 34 | access to the `srv` object described above. 35 | 36 | Function names are to be specified in lower-case characters. 37 | 38 | If you want to run the user-defined function immediately after starting mqttwarn 39 | instead of waiting for the interval to elapse, you might want to add `now=true`. 40 | ```ini 41 | [cron] 42 | pinger = 10.5; now=true 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/examples: -------------------------------------------------------------------------------- 1 | ../examples -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/mqttwarn-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/docs/mqttwarn-logo.png -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | ../README.rst -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | furo 2 | jinja2<4 3 | myst-parser[linkify]<2 4 | sphinx<7 5 | sphinx-copybutton<1 6 | sphinx-togglebutton<1 7 | sphinxext-opengraph<1 8 | -------------------------------------------------------------------------------- /docs/usage/freebsd.md: -------------------------------------------------------------------------------- 1 | (using-freebsd)= 2 | # Installing mqttwarn on FreeBSD 3 | 4 | With this installation method about how to install mqttwarn on [FreeBSD], 5 | you will acquire the [py-mqttwarn] package from the [FreeBSD ports tree], 6 | and install it on your machine. 7 | 8 | ## Synopsis 9 | 10 | ```bash 11 | pkg install sysutils/py-mqttwarn 12 | ``` 13 | 14 | 15 | ## Additional service plugins 16 | 17 | In order to add support for a specific service plugin not bundled with the 18 | default installation, you will need to install its dependencies manually. 19 | 20 | In order to do that, head over to the [`setup.py`] file of mqttwarn, inspect 21 | the list of dependencies, and use the [FreshPorts search] to figure out the 22 | package name of the corresponding dependency on the FreeBSD ports tree, 23 | for example [py-pyserial] or [py-slixmpp]. Then, install the additional 24 | package(s) using [pkg], for example: 25 | ``` 26 | pkg install comms/py-pyserial 27 | pkg install net-im/py-slixmpp 28 | ``` 29 | 30 | If some package is not available there, you may consider installing it from 31 | the [Python package index] using `pip install `, for example: 32 | ``` 33 | pip install pyserial slixmpp 34 | ``` 35 | 36 | 37 | [FreeBSD]: https://www.freebsd.org/ 38 | [FreeBSD ports tree]: https://www.freebsd.org/ports/ 39 | [FreshPorts search]: https://www.freshports.org/search.php 40 | [pkg]: https://man.freebsd.org/cgi/man.cgi?query=pkg 41 | [py-mqttwarn]: https://www.freshports.org/sysutils/py-mqttwarn/ 42 | [py-pyserial]: https://www.freshports.org/comms/py-pyserial/ 43 | [py-slixmpp]: https://www.freshports.org/net-im/py-slixmpp/ 44 | [Python package index]: https://pypi.org/ 45 | [`setup.py`]: https://github.com/mqtt-tools/mqttwarn/blob/main/setup.py 46 | -------------------------------------------------------------------------------- /docs/usage/index.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | .. _installing: 3 | .. _install: 4 | .. _usage: 5 | .. _using: 6 | .. _use: 7 | 8 | ###################### 9 | Installation and usage 10 | ###################### 11 | 12 | This part of the documentation covers the installation and usage of mqttwarn. 13 | The first step to using any software package is getting it properly installed. 14 | Please read this section carefully. 15 | 16 | After successfully installing the software, please follow up to learn about how 17 | to :ref:`configure ` it. 18 | 19 | Installation variants 20 | ===================== 21 | 22 | mqttwarn can be installed natively on your system, or by running an OCI container 23 | image on Docker, Podman, Kubernetes, or friends. Depending on your preferences or 24 | system environment, please use either of those variants: 25 | 26 | - :ref:`using-pip` 27 | - :ref:`using-oci-image` 28 | - :ref:`using-freebsd` 29 | 30 | If you are interested in contributing to mqttwarn, you should setup a development 31 | sandbox, see :ref:`sandbox`. 32 | 33 | Configuration file 34 | ================== 35 | 36 | Before running mqttwarn, you will need a configuration file. 37 | 38 | The path to the configuration file is obtained from the ``MQTTWARNINI`` environment 39 | variable, and defaults to ``mqttwarn.ini`` in the current directory. 40 | On server installations, the default configuration file is located at 41 | ``/etc/mqttwarn/mqttwarn.ini``. 42 | 43 | You can create a configuration file blueprint easily:: 44 | 45 | mqttwarn make-config 46 | 47 | 48 | Running 49 | ======= 50 | 51 | In order to start the program, just type:: 52 | 53 | mqttwarn 54 | 55 | or:: 56 | 57 | MQTTWARNINI=/path/to/mqttwarn.ini mqttwarn 58 | 59 | 60 | 61 | .. toctree:: 62 | :hidden: 63 | 64 | pip 65 | oci 66 | freebsd 67 | standalone 68 | -------------------------------------------------------------------------------- /docs/usage/pip.md: -------------------------------------------------------------------------------- 1 | (using-pip)= 2 | # Installing mqttwarn with pip 3 | 4 | With this installation method, you will acquire a package of mqttwarn from PyPI 5 | and install it on your workstation. We recommend to use a Python virtualenv for 6 | that. 7 | 8 | ## Synopsis 9 | 10 | ```bash 11 | pip install --upgrade mqttwarn 12 | ``` 13 | 14 | You can also add support for a specific service plugin. 15 | 16 | ```bash 17 | pip install --upgrade 'mqttwarn[xmpp]' 18 | ``` 19 | 20 | You can also add support for multiple services, all at once. 21 | 22 | ```bash 23 | pip install --upgrade 'mqttwarn[apprise,asterisk,nsca,desktopnotify,tootpaste,xmpp]' 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/workbench/changelog.rst: -------------------------------------------------------------------------------- 1 | ../../CHANGES.rst -------------------------------------------------------------------------------- /etc/OpenWRT.init: -------------------------------------------------------------------------------- 1 | #!/bin/sh /etc/rc.common 2 | # Script to start mqttwarn as a daemon for OpenWRT 3 | 4 | START=95 5 | STOP=10 6 | 7 | DIR="/overlay/mosquitto/" 8 | BIN="/overlay/mosquitto/mqttwarn" 9 | PIDFILE=/var/run/mqttwarn.pid 10 | 11 | start() { 12 | echo start 13 | cd $DIR 14 | start-stop-daemon -b -S -q -m -p $PIDFILE -x $BIN 15 | } 16 | 17 | stop() { 18 | echo stop 19 | start-stop-daemon -K -q -p $PIDFILE 20 | rm -f $PIDFILE 21 | } 22 | -------------------------------------------------------------------------------- /etc/mqttwarn.default: -------------------------------------------------------------------------------- 1 | # For systemd-based systems (used by mqttwarn.service) 2 | MQTTWARNINI="/etc/mqttwarn/mqttwarn.ini" 3 | MQTTWARN_OPTIONS="" 4 | 5 | # For init-based systems (used by mqttwarn.init) 6 | START_DAEMON=true 7 | VERBOSE=true 8 | -------------------------------------------------------------------------------- /etc/mqttwarn.logrotate: -------------------------------------------------------------------------------- 1 | /var/log/mqttwarn/*.log { 2 | rotate 7 3 | daily 4 | compress 5 | size 2M 6 | nocreate 7 | missingok 8 | postrotate 9 | /bin/systemctl restart mqttwarn 10 | endscript 11 | } 12 | -------------------------------------------------------------------------------- /etc/mqttwarn.openrc: -------------------------------------------------------------------------------- 1 | #!/sbin/openrc-run 2 | 3 | command="/usr/local/bin/mqttwarn" 4 | command_args="${MQTTWARN_OPTIONS}" 5 | command_background=yes 6 | pidfile=/run/mqttwarn.pid 7 | 8 | name="mqttwarn" 9 | description="mqttwarn pluggable mqtt notification service" 10 | 11 | depend() { 12 | need net 13 | } 14 | -------------------------------------------------------------------------------- /etc/mqttwarn.service: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------- 2 | # systemd unit configuration file for mqttwarn 3 | # ---------------------------------------------- 4 | # 5 | # Intro 6 | # ----- 7 | # This systemd script assumes you installed mqttwarn 8 | # using `pip install mqttwarn`. 9 | # 10 | # Setup 11 | # ----- 12 | # 13 | # Prepare and enable the systemd service:: 14 | # 15 | # useradd --create-home --shell /bin/bash mqttwarn 16 | # cp etc/mqttwarn.default /etc/default/mqttwarn 17 | # cp etc/mqttwarn.service /usr/lib/systemd/system/ 18 | # cp etc/mqttwarn.logrotate /etc/logrotate.d/mqttwarn 19 | # mkdir /var/log/mqttwarn 20 | # chown mqttwarn:mqttwarn /var/log/mqttwarn 21 | # systemctl enable mqttwarn 22 | # 23 | # Configuration 24 | # ------------- 25 | # The configuration file is located at /etc/mqttwarn/mqttwarn.ini, 26 | # but the default setting can be changed by amending the 27 | # MQTTWARNINI environment variable defined in /etc/default/mqttwarn. 28 | # 29 | # Setup example configuration:: 30 | # 31 | # mkdir /etc/mqttwarn 32 | # cp mqttwarn.ini.sample /etc/mqttwarn/mqttwarn.ini 33 | # 34 | # Start 35 | # ----- 36 | # :: 37 | # 38 | # systemctl start mqttwarn 39 | # 40 | 41 | [Unit] 42 | Description=mqttwarn pluggable mqtt notification service 43 | Documentation=https://github.com/mqtt-tools/mqttwarn 44 | After=network.target 45 | 46 | [Service] 47 | Type=simple 48 | User=mqttwarn 49 | Group=mqttwarn 50 | LimitNOFILE=65536 51 | Environment='STDOUT=/var/log/mqttwarn/mqttwarn.log' 52 | Environment='STDERR=/var/log/mqttwarn/mqttwarn.log' 53 | EnvironmentFile=/etc/default/mqttwarn 54 | PassEnvironment=MQTTWARNINI 55 | ExecStart=/bin/sh -c 'exec /usr/local/bin/mqttwarn ${MQTTWARN_OPTIONS} >>${STDOUT} 2>>${STDERR}' 56 | KillMode=control-group 57 | Restart=on-failure 58 | 59 | [Install] 60 | WantedBy=multi-user.target 61 | Alias=mqttwarn.service 62 | -------------------------------------------------------------------------------- /etc/supervisor.ini: -------------------------------------------------------------------------------- 1 | [program:mqttwarn] 2 | command = /usr/local/bin/mqttwarn 3 | user = mqttwarn 4 | environment = MQTTWARNINI="/path/to/mqttwarn.ini" 5 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/examples/__init__.py -------------------------------------------------------------------------------- /examples/alexa/alexa.ini: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | [defaults] 4 | hostname = 'localhost' 5 | port = 1883 6 | clientid = 'mqttwarn' 7 | 8 | logfile = 'stream://sys.stderr' 9 | ; one of: CRITICAL, DEBUG, ERROR, INFO, WARN 10 | loglevel = DEBUG 11 | ;logformat = '%(asctime)-15s %(levelname)-8s [%(name)-25s] %(message)s' 12 | 13 | launch = pipe 14 | 15 | [config:pipe] 16 | targets = { 17 | 'alexa_living_room' : [ '/home/pi/shell/alexa-remote-control/saystdin', '-d', 'Living_Room' ], 18 | 'everywhere_group' : [ '/home/pi/shell/alexa-remote-control/announce_stdin', '-d', 'Everywhere' ], 19 | } 20 | 21 | # echo testing from m q t t warn | mosquitto_pub -t 'alexa/living_room' -l 22 | [alexa/living_room] 23 | targets = pipe:alexa_living_room 24 | 25 | # echo Hello world | mosquitto_pub -t 'alexa/everywhere' -l 26 | [alexa/everywhere] 27 | targets = pipe:everywhere_group 28 | -------------------------------------------------------------------------------- /examples/alexa/announce_stdin: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . /home/pi/shell/alexa-remote-control/secrets.sh 4 | 5 | # Example: 6 | # echo test from command line | ./announce_stdin -d Living_Room 7 | 8 | read message 9 | echo ${message} 10 | 11 | env USE_ANNOUNCEMENT_FOR_SPEAK=1 /home/pi/shell/alexa-remote-control/alexa_remote_control.sh ${*} -e speak:"${message}" 12 | -------------------------------------------------------------------------------- /examples/alexa/readme.md: -------------------------------------------------------------------------------- 1 | # Amazon Alexa 2 | 3 | ## About 4 | 5 | An alternative to alexa-notify-me notification (speaker glows yellow and awaits 6 | instruction to play the notification) is for TTS to specific devices or 7 | announce to a speaker group. 8 | 9 | See the examples directory for integration with pipe and the [alexa-remote-control] 10 | shell scripts. 11 | 12 | ## Instructions 13 | 14 | * Download or clone [alexa-remote-control]. 15 | * Edit the [secrets.sh](./secrets.sh) file. 16 | * Ensure paths are correct. 17 | Scripts and `alexa.ini` file assume path `/home/pi/shell/alexa-remote-control`. 18 | * Edit `alexa.ini` file targets with device names and/or group name. 19 | `stdin` for single devices, `announce_stdin` for groups. 20 | * Sanity check, `chmod a+x` on all shell scripts. 21 | 22 | 23 | [alexa-remote-control]: https://github.com/thorsten-gehrig/alexa-remote-control 24 | -------------------------------------------------------------------------------- /examples/alexa/saystdin: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . /home/pi/shell/alexa-remote-control/secrets.sh 4 | 5 | # Example: 6 | # echo test from command line | ./saystdin -d Speaker_Group 7 | 8 | read message 9 | echo ${message} 10 | 11 | /home/pi/shell/alexa-remote-control/alexa_remote_control_plain.sh ${*} -e speak:"${message}" 12 | -------------------------------------------------------------------------------- /examples/alexa/secrets.sh: -------------------------------------------------------------------------------- 1 | # EDIT this script! Add/remove/edit as needed 2 | 3 | export EMAIL='your@email.here.com' 4 | export PASSWORD='your passwod here' 5 | export MFA_SECRET='your 2fa scret here' 6 | export LANGUAGE='en-US' 7 | export TTS_LOCALE='en-EN' 8 | export AMAZON='amazon.com' 9 | export ALEXA='pitangui.amazon.com' 10 | -------------------------------------------------------------------------------- /examples/arduino-temperature/readme.md: -------------------------------------------------------------------------------- 1 | # Arduino » Enrich temperature readings 2 | 3 | 4 | ## About 5 | 6 | Assuming we get, from an Arduino, say, a single numerical value in the payload 7 | of an MQTT message, we want to generate JSON with some additional fields. Using 8 | a [Jinja2] template for the task, does exactly what we need. 9 | 10 | 11 | ## Implementation 12 | 13 | The following target configuration invokes the template: 14 | 15 | ```ini 16 | [arduino/temp] 17 | targets = log:info, http:graylog2 18 | template = temp2json.json 19 | ``` 20 | 21 | The Jinja2 template looks like this. 22 | ```jinja 23 | {# 24 | We expect a single numeric temperature value in `payload'. 25 | Return JSON suitable for Graylog2 (requires `host` and `short_message`). 26 | 27 | Define a data structure in Jinja2 and return it as a JSON string. 28 | Note how transformation data (produced within mqttwarn) is used: 29 | The variables `_dtiso` and `payload` carry the timestamp and the 30 | payload respectively. 31 | #} 32 | {% set data = { 33 | 'host': topic, 34 | 'short_message': "Heat " + payload, 35 | 'tst': _dtiso, 36 | 'temperature': payload, 37 | 'woohooo': 17, 38 | } 39 | %} 40 | {{ data | jsonify }} 41 | ``` 42 | 43 | An example JSON string returned by that template is then passed to the 44 | configured targets. 45 | ```json 46 | {"host": "arduino/temp", "woohooo": 17, "tst": "2014-04-13T09:25:46.247150Z", "temperature": "22", "short_message": "Heat 22"} 47 | ``` 48 | 49 | 50 | [Jinja2]: https://jinja.palletsprojects.com/templates/ 51 | -------------------------------------------------------------------------------- /examples/conftest.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures.ntfy import ntfy_service # noqa:F401 2 | -------------------------------------------------------------------------------- /examples/frigate/.env: -------------------------------------------------------------------------------- 1 | # Software component versions. 2 | MOSQUITTO_VERSION=2.0.15 3 | NTFY_VERSION=latest 4 | 5 | # Broker configuration (Mosquitto). 6 | PORT_MOSQUITTO=1883 7 | 8 | # Notification service configuration (ntfy). 9 | PORT_NTFY=5555 10 | -------------------------------------------------------------------------------- /examples/frigate/.gitignore: -------------------------------------------------------------------------------- 1 | *.jpg 2 | *.jpeg 3 | *.png 4 | -------------------------------------------------------------------------------- /examples/frigate/assets/frigate-event-end.json: -------------------------------------------------------------------------------- 1 | { 2 | "before": { 3 | "id": "1680791459.255384-abcdef", 4 | "camera": "cam-testdrive", 5 | "frame_time": 1680791459.255384, 6 | "snapshot_time": 0, 7 | "label": "goat", 8 | "sub_label": null, 9 | "top_score": 0, 10 | "false_positive": true, 11 | "start_time": 1680791459.255384, 12 | "end_time": null, 13 | "current_zones": [], 14 | "entered_zones": [], 15 | "has_clip": false, 16 | "has_snapshot": false 17 | }, 18 | "after": { 19 | "id": "1680791459.255384-abcdef", 20 | "camera": "cam-testdrive", 21 | "frame_time": 1680791506.638857, 22 | "snapshot_time": 1680791506.638857, 23 | "label": "goat", 24 | "sub_label": null, 25 | "false_positive": false, 26 | "start_time": 1680791459.255384, 27 | "end_time": null, 28 | "current_zones": [], 29 | "entered_zones": [ 30 | "zone1" 31 | ], 32 | "has_clip": true, 33 | "has_snapshot": true 34 | }, 35 | "type": "end" 36 | } 37 | -------------------------------------------------------------------------------- /examples/frigate/assets/frigate-event-false-positive.json: -------------------------------------------------------------------------------- 1 | { 2 | "before": { 3 | "id": "1680791459.255384-abcdef", 4 | "camera": "cam-testdrive", 5 | "frame_time": 1680791459.255384, 6 | "snapshot_time": 0, 7 | "label": "goat", 8 | "sub_label": null, 9 | "top_score": 0, 10 | "false_positive": true, 11 | "start_time": 1680791459.255384, 12 | "end_time": null, 13 | "current_zones": [], 14 | "entered_zones": [], 15 | "has_clip": false, 16 | "has_snapshot": false 17 | }, 18 | "after": { 19 | "id": "1680791459.255384-abcdef", 20 | "camera": "cam-testdrive", 21 | "frame_time": 1680791506.638857, 22 | "snapshot_time": 1680791506.638857, 23 | "label": "goat", 24 | "sub_label": null, 25 | "false_positive": true, 26 | "start_time": 1680791459.255384, 27 | "end_time": null, 28 | "current_zones": [], 29 | "entered_zones": [ 30 | "zone1" 31 | ], 32 | "has_clip": true, 33 | "has_snapshot": true 34 | }, 35 | "type": "new" 36 | } 37 | -------------------------------------------------------------------------------- /examples/frigate/assets/frigate-event-full.json: -------------------------------------------------------------------------------- 1 | { 2 | "before": { 3 | "id": "1680791459.255384-abcdef", 4 | "camera": "cam-testdrive", 5 | "frame_time": 1680791459.255384, 6 | "snapshot_time": 0, 7 | "label": "goat", 8 | "sub_label": null, 9 | "top_score": 0, 10 | "false_positive": true, 11 | "start_time": 1680791459.255384, 12 | "end_time": null, 13 | "score": 0.7, 14 | "box": [ 15 | 0, 16 | 20, 17 | 0, 18 | 20 19 | ], 20 | "area": 400, 21 | "ratio": 1, 22 | "region": [ 23 | 0, 24 | 0, 25 | 320, 26 | 320 27 | ], 28 | "stationary": false, 29 | "motionless_count": 0, 30 | "position_changes": 0, 31 | "current_zones": [], 32 | "entered_zones": [], 33 | "has_clip": false, 34 | "has_snapshot": false 35 | }, 36 | "after": { 37 | "id": "1680791459.255384-abcdef", 38 | "camera": "cam-testdrive", 39 | "frame_time": 1680791506.638857, 40 | "snapshot_time": 1680791506.638857, 41 | "label": "goat", 42 | "sub_label": null, 43 | "top_score": 0.75, 44 | "false_positive": false, 45 | "start_time": 1680791459.255384, 46 | "end_time": null, 47 | "score": 0.8, 48 | "box": [ 49 | 1, 50 | 21, 51 | 1, 52 | 21 53 | ], 54 | "area": 400, 55 | "ratio": 1, 56 | "region": [ 57 | 0, 58 | 0, 59 | 320, 60 | 320 61 | ], 62 | "stationary": false, 63 | "motionless_count": 1, 64 | "position_changes": 2, 65 | "current_zones": [ 66 | "barn" 67 | ], 68 | "entered_zones": [ 69 | "lawn" 70 | ], 71 | "has_clip": true, 72 | "has_snapshot": true 73 | }, 74 | "type": "new" 75 | } 76 | -------------------------------------------------------------------------------- /examples/frigate/assets/frigate-event-new-good.json: -------------------------------------------------------------------------------- 1 | { 2 | "before": { 3 | "id": "1680791459.255384-abcdef", 4 | "camera": "cam-testdrive", 5 | "frame_time": 1680791459.255384, 6 | "snapshot_time": 0, 7 | "label": "goat", 8 | "sub_label": null, 9 | "top_score": 0, 10 | "false_positive": true, 11 | "start_time": 1680791459.255384, 12 | "end_time": null, 13 | "current_zones": [], 14 | "entered_zones": [], 15 | "has_clip": false, 16 | "has_snapshot": false 17 | }, 18 | "after": { 19 | "id": "1680791459.255384-abcdef", 20 | "camera": "cam-testdrive", 21 | "frame_time": 1680791506.638857, 22 | "snapshot_time": 1680791506.638857, 23 | "label": "goat", 24 | "sub_label": null, 25 | "false_positive": false, 26 | "start_time": 1680791459.255384, 27 | "end_time": null, 28 | "current_zones": [ 29 | "barn" 30 | ], 31 | "entered_zones": [ 32 | "lawn" 33 | ], 34 | "has_clip": true, 35 | "has_snapshot": true 36 | }, 37 | "type": "new" 38 | } 39 | -------------------------------------------------------------------------------- /examples/frigate/assets/frigate-event-new-ignored.json: -------------------------------------------------------------------------------- 1 | { 2 | "before": { 3 | "id": "1680791459.255384-abcdef", 4 | "camera": "frontyard", 5 | "frame_time": 1680791459.255384, 6 | "snapshot_time": 0, 7 | "label": "goat", 8 | "sub_label": null, 9 | "top_score": 0, 10 | "false_positive": true, 11 | "start_time": 1680791459.255384, 12 | "end_time": null, 13 | "current_zones": [], 14 | "entered_zones": [], 15 | "has_clip": false, 16 | "has_snapshot": false 17 | }, 18 | "after": { 19 | "id": "1680791459.255384-abcdef", 20 | "camera": "frontyard", 21 | "frame_time": 1680791506.638857, 22 | "snapshot_time": 1680791506.638857, 23 | "label": "goat", 24 | "sub_label": null, 25 | "false_positive": false, 26 | "start_time": 1680791459.255384, 27 | "end_time": null, 28 | "current_zones": [], 29 | "entered_zones": [ 30 | "lawn" 31 | ], 32 | "has_clip": true, 33 | "has_snapshot": true 34 | }, 35 | "type": "new" 36 | } 37 | -------------------------------------------------------------------------------- /examples/frigate/assets/frigate-event-update-good.json: -------------------------------------------------------------------------------- 1 | { 2 | "before": { 3 | "id": "1680791459.255384-abcdef", 4 | "camera": "cam-testdrive", 5 | "frame_time": 1680791459.255384, 6 | "snapshot_time": 0, 7 | "label": "goat", 8 | "sub_label": null, 9 | "top_score": 0, 10 | "false_positive": true, 11 | "start_time": 1680791459.255384, 12 | "end_time": null, 13 | "current_zones": [], 14 | "entered_zones": [], 15 | "has_clip": false, 16 | "has_snapshot": false 17 | }, 18 | "after": { 19 | "id": "1680791459.255384-abcdef", 20 | "camera": "cam-testdrive", 21 | "frame_time": 1680791506.638857, 22 | "snapshot_time": 1680791506.638857, 23 | "label": "goat", 24 | "sub_label": null, 25 | "false_positive": false, 26 | "start_time": 1680791459.255384, 27 | "end_time": null, 28 | "current_zones": [ 29 | "barn" 30 | ], 31 | "entered_zones": [ 32 | "lawn" 33 | ], 34 | "has_clip": true, 35 | "has_snapshot": true 36 | }, 37 | "type": "update" 38 | } 39 | -------------------------------------------------------------------------------- /examples/frigate/assets/frigate-event-update-samezone.json: -------------------------------------------------------------------------------- 1 | { 2 | "before": { 3 | "id": "1680791459.255384-abcdef", 4 | "camera": "cam-testdrive", 5 | "frame_time": 1680791459.255384, 6 | "snapshot_time": 0, 7 | "label": "goat", 8 | "sub_label": null, 9 | "top_score": 0, 10 | "false_positive": true, 11 | "start_time": 1680791459.255384, 12 | "end_time": null, 13 | "current_zones": [], 14 | "entered_zones": [], 15 | "has_clip": false, 16 | "has_snapshot": false 17 | }, 18 | "after": { 19 | "id": "1680791459.255384-abcdef", 20 | "camera": "cam-testdrive", 21 | "frame_time": 1680791506.638857, 22 | "snapshot_time": 1680791506.638857, 23 | "label": "goat", 24 | "sub_label": null, 25 | "false_positive": false, 26 | "start_time": 1680791459.255384, 27 | "end_time": null, 28 | "current_zones": [], 29 | "entered_zones": [], 30 | "has_clip": true, 31 | "has_snapshot": true 32 | }, 33 | "type": "update" 34 | } 35 | -------------------------------------------------------------------------------- /examples/frigate/assets/frigate-event-update-stationary.json: -------------------------------------------------------------------------------- 1 | { 2 | "before": { 3 | "id": "1680791459.255384-abcdef", 4 | "camera": "cam-testdrive", 5 | "frame_time": 1680791459.255384, 6 | "snapshot_time": 0, 7 | "label": "goat", 8 | "sub_label": null, 9 | "top_score": 0, 10 | "false_positive": true, 11 | "start_time": 1680791459.255384, 12 | "end_time": null, 13 | "current_zones": [], 14 | "entered_zones": [], 15 | "has_clip": false, 16 | "has_snapshot": false, 17 | "stationary": true 18 | }, 19 | "after": { 20 | "id": "1680791459.255384-abcdef", 21 | "camera": "cam-testdrive", 22 | "frame_time": 1680791506.638857, 23 | "snapshot_time": 1680791506.638857, 24 | "label": "goat", 25 | "sub_label": null, 26 | "false_positive": false, 27 | "start_time": 1680791459.255384, 28 | "end_time": null, 29 | "current_zones": [], 30 | "entered_zones": [ 31 | "zone1" 32 | ], 33 | "has_clip": true, 34 | "has_snapshot": true, 35 | "stationary": true 36 | }, 37 | "type": "update" 38 | } 39 | -------------------------------------------------------------------------------- /examples/frigate/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | 5 | # --------- 6 | # Mosquitto 7 | # --------- 8 | # https://hub.docker.com/_/eclipse-mosquitto 9 | mosquitto: 10 | image: eclipse-mosquitto:${MOSQUITTO_VERSION} 11 | container_name: mosquitto 12 | command: ["mosquitto", "-c", "/mosquitto-no-auth.conf"] 13 | ports: 14 | - "${PORT_MOSQUITTO}:${PORT_MOSQUITTO}" 15 | 16 | # Define health check for Mosquitto. 17 | healthcheck: 18 | test: [ "CMD", "mosquitto_sub", "-v", "-t", "foobar", "-E" ] 19 | start_period: 1s 20 | interval: 3s 21 | timeout: 10s 22 | retries: 60 23 | 24 | # ---- 25 | # ntfy 26 | # ---- 27 | # https://docs.ntfy.sh/install/#docker 28 | # https://hub.docker.com/r/binwiederhier/ntfy 29 | ntfy: 30 | image: binwiederhier/ntfy:${NTFY_VERSION} 31 | container_name: ntfy 32 | command: > 33 | serve 34 | --base-url="http://localhost:5555" 35 | --attachment-cache-dir="/tmp/ntfy-attachments" 36 | --attachment-expiry-duration="168h" 37 | environment: 38 | # optional: set desired timezone 39 | - TZ=UTC 40 | ports: 41 | - "${PORT_NTFY}:80" 42 | healthcheck: 43 | test: ["CMD-SHELL", "wget -q --tries=1 http://localhost:5555/v1/health -O - | grep -Eo '\"healthy\"\\s*:\\s*true' || exit 1"] 44 | interval: 60s 45 | timeout: 10s 46 | retries: 3 47 | start_period: 40s 48 | 49 | # ------- 50 | # Bundler 51 | # ------- 52 | # Wait for all defined services to be fully available by probing their health 53 | # status, even when using `docker compose up --detach`. 54 | # https://marcopeg.com/2019/docker-compose-healthcheck/ 55 | start-dependencies: 56 | image: dadarek/wait-for-dependencies 57 | depends_on: 58 | mosquitto: 59 | condition: service_healthy 60 | ntfy: 61 | condition: service_healthy 62 | -------------------------------------------------------------------------------- /examples/frigate/frigate.ini: -------------------------------------------------------------------------------- 1 | # Frigate » Forward events and snapshots to Ntfy, using mqttwarn. 2 | # https://mqttwarn.readthedocs.io/en/latest/examples/frigate/README.html 3 | 4 | [defaults] 5 | functions = frigate.py 6 | launch = ntfy, store-image 7 | 8 | status_publish = True 9 | 10 | # This scenario needs two workers, because it needs the headroom of two threads 11 | # running in parallel, to synchronize _two_ distinct Frigate events with each other, 12 | # in order to send out _one_ notification. 13 | num_workers = 2 14 | 15 | 16 | 17 | # ===================== 18 | # Frigate event to ntfy 19 | # ===================== 20 | 21 | # Format: JSON 22 | # Docs: https://docs.frigate.video/integrations/mqtt/#frigateevents 23 | 24 | [config:ntfy] 25 | targets = { 26 | 'test': { 27 | 'url': 'http://username:password@localhost:5555/frigate-testdrive', 28 | 'file': '/tmp/mqttwarn-frigate-{camera}-{label}.png', 29 | 'click': 'https://httpbin.org/anything?camera={event.camera}&label={event.label}&zone={event.entered_zones[0]}', 30 | # Wait for the file to arrive for three quarters of a second, and delete it after reading. 31 | '__settings__': { 32 | 'file_retry_tries': 10, 33 | 'file_retry_interval': 0.075, 34 | 'file_unlink': True, 35 | } 36 | } 37 | } 38 | 39 | [frigate/events] 40 | filter = frigate_events_filter() 41 | alldata = frigate_events() 42 | targets = ntfy:test 43 | title = {event.label} entered {event.entered_zones_str} at {event.time} 44 | format = {event.label} was in {event.current_zones_str} before 45 | 46 | # Limit the alert based on camera/zone. 47 | frigate_skip_rules = { 48 | 'rule-1': {'camera': ['frontyard'], 'entered_zones': ['lawn']}, 49 | } 50 | 51 | 52 | # ===================== 53 | # Frigate image to file 54 | # ===================== 55 | 56 | # Format: Binary (PNG or JPEG) 57 | # Docs: https://docs.frigate.video/integrations/mqtt/#frigatecamera_nameobject_namesnapshot 58 | 59 | [config:store-image] 60 | module = file 61 | targets = { 62 | 'cam-testdrive-goat': ['/tmp/mqttwarn-frigate-cam-testdrive-goat.png'], 63 | 'cam-testdrive-squirrel': ['/tmp/mqttwarn-frigate-cam-testdrive-squirrel.png'], 64 | } 65 | 66 | # Configure `file` plugin to pass through payload 1:1. 67 | append_newline = False 68 | decode_utf8 = False 69 | overwrite = True 70 | 71 | [frigate/+/+/snapshot] 72 | alldata = frigate_snapshot_decode_topic() 73 | targets = store-image:{camera_name}-{object_name} 74 | -------------------------------------------------------------------------------- /examples/frigate/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check prerequisites. 4 | prerequisites="cat convert jq mosquitto_pub wget" 5 | for program in ${prerequisites}; do 6 | if [ ! "$( command -v "${program}" )" ]; then 7 | echo "ERROR: Program not installed: ${program}" 8 | exit 1 9 | fi 10 | done 11 | 12 | # Acquire image for publishing. 13 | if [ ! -f goat.png ]; then 14 | wget -O goat.png https://user-images.githubusercontent.com/453543/231550862-5a64ac7c-bdfa-4509-86b8-b1a770899647.png 15 | fi 16 | 17 | # 1. Publish picture snapshot in PNG format. 18 | mosquitto_pub -f goat.png -t 'frigate/cam-testdrive/goat/snapshot' 19 | 20 | # 2. Publish event in JSON format. 21 | # shellcheck disable=SC2002 22 | cat "assets/frigate-event-new-good.json" | jq -c | mosquitto_pub -t 'frigate/events' -l 23 | -------------------------------------------------------------------------------- /examples/hiveeyes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/examples/hiveeyes/__init__.py -------------------------------------------------------------------------------- /examples/homie/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/examples/homie/__init__.py -------------------------------------------------------------------------------- /examples/homie/homie.ini: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Demonstrate Homie function extensions for mqttwarn 3 | 4 | ; ======== 5 | ; Synopsis 6 | ; ======== 7 | ; 8 | ; Run mqttwarn:: 9 | ; 10 | ; export MQTTWARNINI=examples/homie/homie.ini 11 | ; mqttwarn 12 | ; 13 | ; Send some homie-like data:: 14 | ; 15 | ; mosquitto_pub -t homie/bee1/weight/value -m 42.42 16 | 17 | 18 | ; ================== 19 | ; Base configuration 20 | ; ================== 21 | 22 | [defaults] 23 | hostname = 'localhost' 24 | clientid = 'mqttwarn' 25 | 26 | ; logging 27 | logformat = '%(asctime)-15s %(levelname)-5s [%(module)s] %(message)s' 28 | logfile = stream://sys.stderr 29 | 30 | ; one of: CRITICAL, DEBUG, ERROR, INFO, WARN 31 | #loglevel = INFO 32 | loglevel = DEBUG 33 | 34 | ; enable service providers 35 | launch = log, file 36 | 37 | ; number of notification dispatcher threads 38 | num_workers = 3 39 | 40 | ; path to file containing self-defined functions 41 | functions = 'examples.homie.homie' 42 | 43 | 44 | ; ================ 45 | ; check_mk routing 46 | ; ================ 47 | 48 | ; See also https://github.com/mqtt-tools/mqttwarn/wiki/Incorporating-topic-names#incorporate-topic-names-into-topic-targets 49 | 50 | [check_mk_universal] 51 | topic = homie/+/+/value 52 | datamap = decode_homie_topic() 53 | targets = file:cmk_spool 54 | format = <<<<{device}>>>>\n<<>>\n 0 {node} {node}={payload} {node}: {payload} 55 | 56 | [config:file] 57 | append_newline = True 58 | overwrite = True 59 | targets = { 60 | 'cmk_spool': ['/var/lib/check_mk_agent/spool/300{device}-{node}'], 61 | } 62 | 63 | 64 | ; =============== 65 | ; Regular logging 66 | ; =============== 67 | 68 | [homie-logging] 69 | ; Just log all incoming messages 70 | topic = homie/# 71 | targets = log:info 72 | 73 | [config:log] 74 | targets = { 75 | 'debug' : [ 'debug' ], 76 | 'info' : [ 'info' ], 77 | 'warn' : [ 'warn' ], 78 | 'crit' : [ 'crit' ], 79 | 'error' : [ 'error' ] 80 | } 81 | 82 | -------------------------------------------------------------------------------- /examples/homie/homie.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Homie function extensions for mqttwarn 3 | import re 4 | 5 | 6 | # ------------------------------------------ 7 | # Synopsis 8 | # ------------------------------------------ 9 | # 10 | # Run mqttwarn:: 11 | # 12 | # export MQTTWARNINI=examples/homie/homie.ini 13 | # mqttwarn 14 | # 15 | # Send some homie-like data:: 16 | # 17 | # mosquitto_pub -t homie/bee1/weight/value -m 42.42 18 | 19 | 20 | def decode_homie_topic(topic): 21 | """ 22 | Split Homie-style MQTT topic path into segments for 23 | enriching transformation data inside mqttwarn. 24 | """ 25 | if type(topic) == str: 26 | try: 27 | pattern = r'^(?P.+?)/(?P.+?)/(?P.+?)/(?P.+?)$' 28 | p = re.compile(pattern) 29 | m = p.match(topic) 30 | topology = m.groupdict() 31 | except: 32 | topology = {} 33 | return topology 34 | return None 35 | 36 | -------------------------------------------------------------------------------- /examples/mediaplayer/mqttwarn-mplayer.ini: -------------------------------------------------------------------------------- 1 | # Simple MQTT media player using mqttwarn and mplayer. 2 | # https://mqttwarn.readthedocs.io/en/latest/examples/mediaplayer/readme.html 3 | 4 | [defaults] 5 | launch = execute 6 | 7 | [config:execute] 8 | targets = { 9 | 'mediaplayer-play': [ 'mplayer', '-volume', '80', '[TEXT]' ], 10 | } 11 | 12 | [mediaplayer/play] 13 | targets = execute:mediaplayer-play 14 | -------------------------------------------------------------------------------- /examples/mediaplayer/readme.md: -------------------------------------------------------------------------------- 1 | {#mqtt-media-player} 2 | # Simple MQTT media player 3 | 4 | 5 | ## About 6 | 7 | The idea is to implement a simple MQTT media player on Linux, which can be used to play TTS 8 | messages from Home Assistant. Home Assistant renders the TTS stream as an MP3 and makes it 9 | available on its HTTP server, so a corresponding command using `mplayer` to play the audio 10 | resource would look like this. 11 | ```shell 12 | mplayer -volume 80 http://home.assistant.address:8123/api/tts_proxy/6a0efdf280bf8c79a.mp3 13 | ``` 14 | 15 | ## Configuration 16 | 17 | The solution for this will be implemented using mqttwarn's [](#execute) service plugin, which 18 | can be used to invoke programs, and interpolate MQTT payload data. 19 | 20 | :::{literalinclude} mqttwarn-mplayer.ini 21 | :language: ini 22 | ::: 23 | 24 | 25 | ## Usage 26 | Using three terminal sessions, you can exercise the example interactively. First, let's start 27 | the [Mosquitto] MQTT broker. 28 | ```shell 29 | docker run --name=mosquitto -it --rm --publish=1883:1883 eclipse-mosquitto:2.0 mosquitto -c /mosquitto-no-auth.conf 30 | ``` 31 | Let's acquire the `mqttwarn-mplayer.ini` configuration file, and start `mqttwarn`. 32 | ```shell 33 | wget https://github.com/mqtt-tools/mqttwarn/raw/main/examples/mediaplayer/mqttwarn-mplayer.ini 34 | mqttwarn --config-file=mqttwarn-mplayer.ini 35 | ``` 36 | Now, when publishing the URL to the audio resource on the designated MQTT topic, `mqttwarn` will 37 | invoke the `mplayer` command as instructed. 38 | ```shell 39 | echo 'http://home.assistant.address:8123/api/tts_proxy/6a0efdf280bf8c79a.mp3' | \ 40 | mosquitto_pub -t 'mediaplayer/play' -l 41 | ``` 42 | 43 | 44 | [Home Assistant]: https://www.home-assistant.io/ 45 | [Mosquitto]: https://mosquitto.org 46 | -------------------------------------------------------------------------------- /examples/owntracks-ntfy/mqttwarn-owntracks.ini: -------------------------------------------------------------------------------- 1 | # Forward OwnTracks low-battery warnings to ntfy. 2 | # https://mqttwarn.readthedocs.io/en/latest/examples/owntracks-ntfy/readme.html 3 | 4 | [defaults] 5 | functions = mqttwarn-owntracks.py 6 | launch = ntfy 7 | 8 | [config:ntfy] 9 | targets = {'testdrive': 'https://ntfy.sh/testdrive'} 10 | 11 | [owntracks/#] 12 | filter = owntracks_batteryfilter() 13 | format = My phone battery is getting low ({batt}%)! 14 | targets = ntfy:testdrive 15 | -------------------------------------------------------------------------------- /examples/owntracks-ntfy/mqttwarn-owntracks.py: -------------------------------------------------------------------------------- 1 | """ 2 | Forward OwnTracks low-battery warnings to ntfy. 3 | https://mqttwarn.readthedocs.io/en/latest/examples/owntracks-ntfy/readme.html 4 | """ 5 | import json 6 | 7 | 8 | def owntracks_batteryfilter(topic: str, message: str): 9 | ignore = True 10 | try: 11 | data = dict(json.loads(message).items()) 12 | except: 13 | data = None 14 | 15 | if data and "batt" in data and data["batt"] is not None: 16 | ignore = int(data["batt"]) > 20 17 | 18 | return ignore 19 | -------------------------------------------------------------------------------- /examples/readme.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This section contains a few examples demonstrating what you can do with mqttwarn, 4 | and about how mqttwarn can be used with more advanced configurations. 5 | 6 | :::{toctree} 7 | :maxdepth: 1 8 | alexa/readme.md 9 | arduino-temperature/readme.md 10 | frigate/README.rst 11 | mediaplayer/readme.md 12 | owntracks-ntfy/readme.md 13 | warntoggle/README.rst 14 | zabbix-iot/readme.md 15 | ::: 16 | -------------------------------------------------------------------------------- /examples/warntoggle/README.rst: -------------------------------------------------------------------------------- 1 | ################################ 2 | Button toggle with web interface 3 | ################################ 4 | 5 | About 6 | ===== 7 | 8 | A custom function to be used when configuring streams in ``mqttwarn``. 9 | ``warntoggle`` makes it easy to toggle notifications "on" or "off" 10 | through a simple web interface. 11 | 12 | Implementation 13 | ============== 14 | 15 | - ``mqttwarn.ini`` has a section ``[+/temperature]``, which applies to all MQTT 16 | messages received on matching topics. This section includes a filter, 17 | referring to a user-defined function. 18 | - When an MQTT message is received, the user-defined function is triggered. 19 | - The user-defined function checks a JSON file for the topic, and based on 20 | ``TRUE`` or ``FALSE`` setting, will return a message back to ``mqttwarn``, to 21 | either allow the notification to happen or not. 22 | - If an MQTT topic was not found within the aforementioned JSON file, it will be 23 | added and set with a configurable default value. 24 | - For the user, an accompanying web script allows the toggle value to be changed. 25 | 26 | Installation 27 | ============ 28 | 29 | - Copy the ``togglestate()`` function from ``mqttwarn/customfunctions.py`` to 30 | your own user-defined functions file, or copy the entire file and refer to it 31 | in ``mqttwarn.ini`` with a ``functions = 'customfunctions.py'`` directive. 32 | - Copy the content of the ``www`` folder to a web server on the same host as 33 | ``mqttwarn``, ensure Python is enabled for the server, and ``warntoggle.json`` 34 | is writeable by the web server. 35 | - Create a symbolic link from ``/etc/mqttwarn/warntoggle.json`` to ``/var/www/html/warntoggle.json`, 36 | adjusted for your local situation. Alternatively, configure the filename inside 37 | the custom function where it now says ``filename = "warntoggle.json"``, to 38 | contain an absolute path. 39 | - For each stream you wish to be considered by ``warntoggle``, add a line 40 | ``filter = togglestate()`` to ``mqttwarn.ini``. This must be done inside each 41 | stream section. 42 | -------------------------------------------------------------------------------- /examples/warntoggle/mqttwarn/customfunctions.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def togglestate(topic, payload, section, srv): 5 | filename = "warntoggle.json" 6 | default_topicblock = False 7 | 8 | try: 9 | with open(filename) as infile: 10 | toggles = json.load(infile) 11 | infile.close() 12 | 13 | if topic in toggles: 14 | # file found, topic found 15 | topicblock = toggles[topic] 16 | srv.logging.debug('togglestate() was called from the {} section and found {} in {}'.format( 17 | section, topic, filename)) 18 | else: 19 | # file found, adding new topic 20 | toggles[topic] = default_topicblock 21 | with open(filename, 'w') as outfile: 22 | json.dump(toggles, outfile) 23 | outfile.close() 24 | 25 | topicblock = default_topicblock 26 | srv.logging.debug('togglestate() was called from the {} section, did not find {} in {}'.format( 27 | section, topic, filename)) 28 | srv.logging.debug('togglestate() added {} to {} with blocking set to {}'.format( 29 | topic, filename, topicblock)) 30 | 31 | except Exception as e: 32 | # file not found or other error 33 | topicblock = default_topicblock 34 | srv.logging.debug('togglestate() encountered an error: {}'.format(e)) 35 | 36 | srv.logging.debug('togglestate() will return {}'.format(topicblock)) 37 | return topicblock 38 | -------------------------------------------------------------------------------- /examples/warntoggle/www/warntoggle.json: -------------------------------------------------------------------------------- 1 | { 2 | "livingroom/temperature": true, 3 | "bedroom/temperature": true, 4 | "bedroom/lights": false 5 | } 6 | -------------------------------------------------------------------------------- /examples/warntoggle/www/warntoggle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import json 4 | 5 | def index(req): 6 | 7 | filename = "/var/www/html/warntoggle/warntoggle.json" 8 | writetoggles = {} 9 | readtoggles = {} 10 | scriptname = __file__.split('/')[-1:][0] 11 | 12 | # write form data to the file if applicable 13 | 14 | if bool(req.form): 15 | 16 | for k,v in req.form.items(): 17 | writetoggles[k] = eval(str(v)) 18 | 19 | with open(filename, 'w') as outfile: 20 | json.dump(writetoggles, outfile) 21 | outfile.close() 22 | 23 | # read data from the file 24 | 25 | with open(filename, 'r') as infile: 26 | readtoggles = json.load(infile) 27 | infile.close() 28 | 29 | # display the form 30 | 31 | html = "

mqttwarn notification toggles

" 32 | html += "" 33 | html += "" 34 | 35 | for key in sorted(readtoggles.keys()): 36 | if readtoggles[key] == True: 37 | block_checked = " checked" 38 | notify_checked = "" 39 | else: 40 | block_checked = "" 41 | notify_checked = " checked" 42 | 43 | html += "" 44 | 45 | html += "
" + key + "Block Notify

" 46 | html += " reload" 47 | html += "" 48 | html += "" 49 | 50 | return html 51 | -------------------------------------------------------------------------------- /examples/zabbix-iot/mqttwarn-zabbix-iot.ini: -------------------------------------------------------------------------------- 1 | [defaults] 2 | functions = 'mqttwarn-zabbix-iot.py' 3 | launch = zabbix 4 | 5 | [config:zabbix] 6 | targets = { 7 | 't1' : [ 'localhost', 10051 ], 8 | } 9 | 10 | [tele/#] 11 | alldata = decode_for_zabbix() 12 | targets = zabbix:t1 13 | -------------------------------------------------------------------------------- /examples/zabbix-iot/mqttwarn-zabbix-iot.py: -------------------------------------------------------------------------------- 1 | def decode_for_zabbix(topic, data, srv=None): 2 | status_key = None 3 | 4 | # the first part (part[0]) is always tele 5 | # the second part (part[1]) is the device, the value comes from 6 | # the third part (part[2]) is the name of the metric (e.g. temperature/humidity/voltage...) 7 | parts = topic.split('/') 8 | client = parts[1] 9 | key = parts[2] 10 | 11 | return dict(client=client, key=key, status_key=status_key) 12 | -------------------------------------------------------------------------------- /mqttwarn/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2014-2023 The mqttwarn developers 3 | 4 | __author__ = "Jan-Piet Mens , Ben Jones " 5 | __copyright__ = "Copyright 2014-2022 Jan-Piet Mens" 6 | __license__ = "Eclipse Public License - v 2.0 (http://www.eclipse.org/legal/epl-2.0/)" 7 | 8 | try: 9 | from importlib.metadata import version 10 | except ImportError: # pragma: nocover 11 | from importlib_metadata import version # type: ignore[no-redef] 12 | 13 | __version__ = version("mqttwarn") 14 | -------------------------------------------------------------------------------- /mqttwarn/__main__.py: -------------------------------------------------------------------------------- 1 | import mqttwarn.commands 2 | 3 | mqttwarn.commands.run() 4 | -------------------------------------------------------------------------------- /mqttwarn/examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/mqttwarn/examples/__init__.py -------------------------------------------------------------------------------- /mqttwarn/services/README.md: -------------------------------------------------------------------------------- 1 | This directory contains one plugin file (`.py`) per service. 2 | -------------------------------------------------------------------------------- /mqttwarn/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/mqttwarn/services/__init__.py -------------------------------------------------------------------------------- /mqttwarn/services/alexa-notify-me.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | __author__ = 'Bram Hendrickx' 5 | __copyright__ = 'Copyright 2016 Bram Hendrickx' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | Original script by Bram Hendrickx. https://github.com/mqtt-tools/mqttwarn/blob/main/mqttwarn/services/ifttt.py 9 | Modified to work with notify-me app for Alexa. http://www.thomptronics.com/notify-me 10 | """ 11 | 12 | __author__ = 'Michael Brougham' 13 | __copyright__ = 'Copyright 2018 Michael Brougham' 14 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 15 | 16 | import json 17 | import requests 18 | 19 | 20 | def plugin(srv, item): 21 | """ expects (key) in addrs """ 22 | 23 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 24 | 25 | try: 26 | srv.logging.debug("Sending to NotifyMe service") 27 | body = json.dumps({ 28 | "notification": item.message, 29 | "accessCode": item.addrs[0] 30 | }) 31 | 32 | response = requests.post(url="https://api.notifymyecho.com/v1/NotifyMe", data=body) 33 | response.raise_for_status() 34 | 35 | srv.logging.debug("Successfully sent to NotifyMe service") 36 | 37 | except Exception as e: 38 | srv.logging.warning("Failed to send message to NotifyMe service: %s" % e) 39 | return False 40 | 41 | return True 42 | -------------------------------------------------------------------------------- /mqttwarn/services/amqp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Jan-Piet Mens ' 5 | __copyright__ = 'Copyright 2014 Jan-Piet Mens' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | import pytest 9 | 10 | puka = pytest.importorskip("puka") 11 | 12 | 13 | def plugin(srv, item): 14 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 15 | 16 | uri = item.config['uri'] 17 | 18 | exchange, routing_key = item.addrs 19 | 20 | try: 21 | srv.logging.debug("AMQP publish to %s [%s/%s]" % (item.target, exchange, routing_key)) 22 | 23 | client = puka.Client(uri) 24 | promise = client.connect() 25 | client.wait(promise) 26 | 27 | headers = { 28 | 'content_type': 'text/plain', 29 | 'x-agent': 'mqttwarn', 30 | 'delivery_mode': 1, 31 | } 32 | promise = client.basic_publish(exchange=exchange, 33 | routing_key=routing_key, 34 | headers=headers, 35 | body=item.message) 36 | client.wait(promise) 37 | client.close() 38 | 39 | srv.logging.debug("Successfully published AMQP notification") 40 | except Exception as e: 41 | srv.logging.warning("Error on AMQP publish to %s [%s/%s]: %s" % (item.target, exchange, routing_key, e)) 42 | return False 43 | 44 | return True 45 | -------------------------------------------------------------------------------- /mqttwarn/services/apns.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Jan-Piet Mens ' 5 | __copyright__ = 'Copyright 2014 Jan-Piet Mens' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | import json 9 | try: 10 | from apns import APNs, Payload 11 | except: 12 | pass 13 | 14 | 15 | def plugin(srv, item): 16 | addrs = item.addrs 17 | data = item.data 18 | text = item.message 19 | 20 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 21 | 22 | try: 23 | cert_file, key_file = addrs 24 | except: 25 | srv.logging.warning("Incorrect service configuration") 26 | return False 27 | 28 | if 'apns_token' not in data: 29 | srv.logging.warning("Cannot notify via APNS: apns_token is missing") 30 | return False 31 | 32 | apns_token = data['apns_token'] 33 | 34 | custom = {} 35 | try: 36 | payload = data['payload'] 37 | mdata = json.loads(payload) 38 | if 'custom' in mdata: 39 | custom = mdata['custom'] 40 | except: 41 | pass 42 | 43 | apns = APNs(use_sandbox=False, cert_file=cert_file, key_file=key_file) 44 | 45 | pload = Payload(alert=text, custom=custom, sound="default", badge=1) 46 | apns.gateway_server.send_notification(apns_token, pload) 47 | 48 | srv.logging.debug("Successfully published APNS notification to %s" % apns_token) 49 | 50 | return True 51 | -------------------------------------------------------------------------------- /mqttwarn/services/apprise.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from mqttwarn.services.apprise_single import plugin 4 | 5 | warnings.warn("`mqttwarn.services.apprise` will be removed in a future release of mqttwarn. " 6 | "Please use `mqttwarn.services.apprise_single` or `mqttwarn.services.apprise_multi` instead.", 7 | category=DeprecationWarning) 8 | -------------------------------------------------------------------------------- /mqttwarn/services/apprise_single.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __author__ = 'Andreas Motl ' 4 | __copyright__ = 'Copyright 2020-2021 Andreas Motl' 5 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 6 | 7 | # https://github.com/caronc/apprise#developers 8 | from collections import OrderedDict 9 | 10 | import apprise 11 | 12 | from mqttwarn.services.apprise_util import obtain_apprise_arguments, add_url_params, get_all_template_argument_names 13 | 14 | APPRISE_ALL_ARGUMENT_NAMES = get_all_template_argument_names() 15 | 16 | 17 | def plugin(srv, item): 18 | """Send a message to a single Apprise plugin.""" 19 | 20 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 21 | 22 | sender = item.config.get('sender') 23 | sender_name = item.config.get('sender_name') 24 | baseuri = item.config['baseuri'] 25 | addresses = item.addrs 26 | title = item.title 27 | body = item.message 28 | 29 | try: 30 | srv.logging.debug("Sending notification to Apprise. target=%s, addresses=%s" % (item.target, addresses)) 31 | to = ','.join(addresses) 32 | 33 | # Disable the Apprise rate limiting subsystem. 34 | try: 35 | from apprise.plugins.NotifyBase import NotifyBase 36 | NotifyBase.request_rate_per_sec = 0 37 | except ImportError: 38 | pass 39 | 40 | # Create an Apprise instance. 41 | apobj = apprise.Apprise(asset=apprise.AppriseAsset(async_mode=False)) 42 | 43 | # Collect URL parameters. 44 | params = OrderedDict() 45 | 46 | # Obtain and apply all possible Apprise parameters from data dictionary. 47 | params.update(obtain_apprise_arguments(item, APPRISE_ALL_ARGUMENT_NAMES)) 48 | 49 | # Apply addressee information. 50 | if sender: 51 | params["from"] = sender 52 | if to: 53 | params["to"] = to 54 | if sender_name: 55 | params["name"] = sender_name 56 | 57 | # Add parameters to Apprise notification URL. 58 | uri = add_url_params(baseuri, params) 59 | apobj.add(uri) 60 | 61 | # Submit notification. 62 | outcome = apobj.notify( 63 | body=body, 64 | title=title, 65 | ) 66 | 67 | if outcome: 68 | srv.logging.info("Successfully sent message using Apprise") 69 | return True 70 | 71 | else: 72 | srv.logging.error("Sending message using Apprise failed") 73 | return False 74 | 75 | except Exception as e: 76 | srv.logging.error("Sending message using Apprise failed. target=%s, error=%s" % (item.target, e)) 77 | return False 78 | -------------------------------------------------------------------------------- /mqttwarn/services/apprise_util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2021-2023 The mqttwarn developers 3 | from __future__ import absolute_import 4 | 5 | from functools import lru_cache 6 | from urllib.parse import urlparse, urlencode 7 | 8 | from apprise import Apprise, ContentLocation 9 | 10 | from mqttwarn.model import ProcessorItem 11 | 12 | 13 | @lru_cache(maxsize=None) 14 | def get_all_template_argument_names(): 15 | """ 16 | Inquire all possible parameter names from all Apprise plugins. 17 | """ 18 | a = Apprise(asset=None, location=ContentLocation.LOCAL) 19 | results = a.details() 20 | plugin_infos = results['schemas'] 21 | 22 | all_arg_names = [] 23 | for plugin_info in plugin_infos: 24 | arg_names = plugin_info["details"]["args"].keys() 25 | all_arg_names += arg_names 26 | 27 | return sorted(set(all_arg_names)) 28 | 29 | 30 | def obtain_apprise_arguments(item: ProcessorItem, arg_names: list) -> dict: 31 | """ 32 | Obtain eventual Apprise parameters from data dictionary. 33 | """ 34 | params = dict() 35 | for arg_name in arg_names: 36 | if isinstance(item.data, dict) and arg_name in item.data: 37 | params[arg_name] = item.data[arg_name] 38 | return params 39 | 40 | 41 | def add_url_params(url: str, params: dict) -> str: 42 | """ 43 | Serialize query parameter dictionary and add it to URL. 44 | """ 45 | url_parsed = urlparse(url) 46 | if params: 47 | seperator = "?" 48 | if url_parsed.query: 49 | seperator = "&" 50 | url += seperator + urlencode(params) 51 | return url 52 | -------------------------------------------------------------------------------- /mqttwarn/services/asterisk.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Artem Alexandrov ' 5 | __copyright__ = 'Copyright 2014 Artem Alexandrov' 6 | __license__ = """Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)""" 7 | 8 | # Please install pyst2 9 | import asterisk.manager 10 | 11 | def plugin(srv, item): 12 | 13 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 14 | 15 | host = item.config['host'] 16 | port = item.config['port'] 17 | username = item.config['username'] 18 | password = item.config['password'] 19 | extension = item.config['extension'] 20 | context = item.config['context'] 21 | 22 | gateway = item.addrs[0] 23 | number = item.addrs[1] 24 | title = item.title 25 | message = item.message 26 | 27 | try: 28 | manager = asterisk.manager.Manager() 29 | manager.connect(host, port) 30 | response = manager.login(username, password) 31 | srv.logging.debug("Authentication {}".format(response)) 32 | channel = gateway + number 33 | channel_vars = {'text': message} 34 | # originate the call 35 | response = manager.originate(channel, extension, context=context, priority='1', caller_id=extension, variables=channel_vars) 36 | srv.logging.info("Call {}".format(response)) 37 | manager.logoff() 38 | except asterisk.manager.ManagerSocketException as e: 39 | srv.logging.error("Error connecting to the manager: {}".format(e)) 40 | return False 41 | except asterisk.manager.ManagerAuthException as e: 42 | srv.logging.error("Error logging in to the manager: {}".format(e)) 43 | return False 44 | except asterisk.manager.ManagerException as e: 45 | srv.logging.error("Error: {}".format(e)) 46 | return False 47 | 48 | # Remember to clean up 49 | finally: 50 | try: 51 | manager.close() 52 | except asterisk.manager.ManagerSocketException: # pragma: no cover 53 | pass 54 | 55 | return True 56 | -------------------------------------------------------------------------------- /mqttwarn/services/autoremote.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | __author__ = 'Bram Hendrickx' 5 | __copyright__ = 'Copyright 2016 Bram Hendrickx' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | Original script by Bram Hendrickx. https://github.com/mqtt-tools/mqttwarn/blob/main/mqttwarn/services/ifttt.py 9 | Modified to work with the autoremote api https://joaoapps.com/autoremote/ 10 | """ 11 | 12 | __author__ = 'Michael Brougham' 13 | __copyright__ = 'Copyright 2018 Michael Brougham' 14 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 15 | 16 | import requests 17 | 18 | 19 | def plugin(srv, item): 20 | ''' expects (apikey, password, target, group, ttl) in addrs ''' 21 | 22 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 23 | 24 | try: 25 | srv.logging.debug("Sending to autoremote service") 26 | params = { 27 | 'key': item.addrs[0], 28 | 'message': item.message, 29 | 'target': item.addrs[2], 30 | 'sender': item.topic, 31 | 'password': item.addrs[1], 32 | 'ttl': item.addrs[4], 33 | 'collapseKey': item.addrs[3], 34 | } 35 | requests.get('https://autoremotejoaomgcd.appspot.com/sendmessage', params=params) 36 | srv.logging.debug("Successfully sent to autoremote service") 37 | 38 | except Exception as e: 39 | srv.logging.warning("Failed to send message to autoremote service: %s" % e) 40 | return False 41 | 42 | return True 43 | -------------------------------------------------------------------------------- /mqttwarn/services/azure_iot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from six import string_types 4 | 5 | __author__ = 'Morten Høybye Frederiksen ' 6 | __copyright__ = 'Copyright 2016-2020 Morten Høybye Frederiksen' 7 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 8 | 9 | from builtins import str 10 | import paho.mqtt.publish as mqtt 11 | import paho.mqtt.client as mqttclient 12 | import ssl 13 | 14 | def plugin(srv, item): 15 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 16 | 17 | # addrs is a list[] containing device id and sas token 18 | deviceid, sastoken = item.addrs 19 | 20 | # iot hub name and qos is stored in config 21 | iothubname = item.config['iothubname'] 22 | qos = int(item.config.get('qos', 0)) 23 | if qos < 0 or qos > 1: 24 | srv.logging.error("Only QoS 0 or 1 allowed for Azure IoT Hub, not '%s'" % str(qos)) 25 | return False 26 | 27 | # connection info... 28 | params = { 29 | 'hostname': iothubname + ".azure-devices.net", 30 | 'port': 8883, 31 | 'protocol': mqttclient.MQTTv311, 32 | 'qos': qos, 33 | 'retain': False, 34 | 'client_id': deviceid, 35 | } 36 | auth = { 37 | 'username': iothubname + ".azure-devices.net/" + 38 | deviceid + "/?api-version=2018-06-30", 39 | 'password': sastoken 40 | } 41 | tls = { 42 | 'ca_certs': None, 43 | 'certfile': None, 44 | 'keyfile': None, 45 | 'tls_version': ssl.PROTOCOL_TLSv1_2, 46 | 'ciphers': None, 47 | 'cert_reqs': ssl.CERT_NONE 48 | } 49 | 50 | # prepare topic 51 | d2c_topic = "devices/" + deviceid + "/messages/events/" 52 | 53 | # prepare payload 54 | try: 55 | if isinstance(item.message, string_types): 56 | payload = bytearray(item.message, 'utf8') 57 | else: 58 | payload = item.message 59 | except Exception as e: 60 | srv.logging.error("Unable to prepare message for target=%s: %s" % (item.target, str(e))) 61 | return False 62 | 63 | # publish... 64 | try: 65 | srv.logging.debug("Publishing to Azure IoT Hub for target=%s (%s): %s '%s'" % (item.target, deviceid, d2c_topic, str(payload))) 66 | mqtt.single(d2c_topic, payload, auth=auth, tls=tls, **params) 67 | except Exception as e: 68 | srv.logging.error("Unable to publish to Azure IoT Hub for target=%s (%s): %s" % (item.target, deviceid, str(e))) 69 | return False 70 | 71 | return True 72 | -------------------------------------------------------------------------------- /mqttwarn/services/carbon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Jan-Piet Mens ' 5 | __copyright__ = 'Copyright 2014 Jan-Piet Mens' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | import time 9 | import socket 10 | 11 | 12 | def plugin(srv, item): 13 | 14 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 15 | 16 | # item.config is brought in from the configuration file 17 | config = item.config 18 | 19 | # addrs is a list[] associated with a particular target. 20 | 21 | try: 22 | carbon_host, carbon_port = item.addrs 23 | carbon_port = int(carbon_port) 24 | except: 25 | srv.logging.error("Configuration for target `carbon' is incorrect") 26 | return False 27 | 28 | # If the incoming payload has been transformed, use that, 29 | # else the original payload 30 | text = item.message 31 | 32 | try: 33 | parts = text.split() 34 | except: 35 | srv.logging.error("target `carbon': cannot split string") 36 | return False 37 | 38 | parts_count = len(parts) 39 | if parts_count == 1: 40 | metric_name = item.data.get('topic', 'ohno').replace('/', '.') 41 | value = parts[0] 42 | tics = int(time.time()) 43 | elif parts_count == 2: 44 | metric_name = parts[0] 45 | value = parts[1] 46 | tics = int(time.time()) 47 | elif parts_count == 3: 48 | metric_name = parts[0] 49 | value = parts[1] 50 | tics = int(parts[2]) 51 | else: 52 | srv.logging.error("target `carbon': error decoding message") 53 | return False 54 | 55 | if metric_name.startswith('.'): # omit dot there caused by useless leading slash in topic 56 | metric_name = metric_name[1:] 57 | carbon_msg = "%s %s %d" % (metric_name, value, tics) 58 | srv.logging.debug("Sending to carbon: %s" % (carbon_msg)) 59 | carbon_msg = carbon_msg + "\n" 60 | try: 61 | sock = socket.socket() 62 | sock.connect((carbon_host, carbon_port)) 63 | sock.sendall(carbon_msg) 64 | sock.close() 65 | except Exception as e: 66 | srv.logging.warning("Cannot send to carbon service %s:%d: %s" % (carbon_host, carbon_port, e)) 67 | return False 68 | 69 | return True 70 | -------------------------------------------------------------------------------- /mqttwarn/services/celery.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Orhan Hirsch ' 5 | __copyright__ = 'Copyright 2017 Orhan Hirsch' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | import json 9 | import celery 10 | 11 | 12 | def plugin(srv, item): 13 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 14 | 15 | config = item.config 16 | 17 | app = celery.Celery( 18 | config['app_name'], 19 | broker=config['broker_url'] 20 | ) 21 | 22 | for target in item.addrs: 23 | message = item.message 24 | try: 25 | if target['message_format'] == 'json': 26 | message = json.loads(message) 27 | app.send_task(target['task'], [message]) 28 | except Exception as e: 29 | srv.logging.warning("Error: %s" % e) 30 | return False 31 | 32 | return True 33 | -------------------------------------------------------------------------------- /mqttwarn/services/chromecast.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Text To Speech (TTS) for Chromecast devices, including Google Home Speakers 5 | 6 | pip install pychromecast 7 | 8 | See also https://github.com/skorokithakis/catt. 9 | """ 10 | 11 | __author__ = 'Chris Clark ' 12 | __copyright__ = 'Copyright 2020 Chris Clark' 13 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 14 | 15 | 16 | import os 17 | from urllib.parse import urlencode 18 | 19 | 20 | def plugin(srv, item): 21 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 22 | 23 | message = item.message 24 | mime_type = item.config.get('mimetype', 'audio/mp3') # Google translate emits mp3, NOTE Chromecast devices appear to not care if incorrect 25 | base_url = item.config.get('baseuri', os.environ.get('GOOGLE_TRANSLATE_URL', 'https://translate.google.com/translate_tts?')) 26 | lang = item.config.get('lang', 'en') # English 27 | 28 | # Generate a Google Translate (compatible) URL to generate TTS 29 | vars = { 30 | 'q': message, 31 | 'l': lang, 32 | 'tl': lang, 33 | 'client': 'tw-ob', 34 | 'ttsspeed': 1, 35 | 'total': 1, 36 | 'ie': 'UTF-8', 37 | # looks like can get away with out 'textlen' 38 | } 39 | url = base_url + urlencode(vars) 40 | 41 | 42 | # TODO disable pychromecast library logging? 43 | import pychromecast # Some plugin systems want lazy loading, defer import until about to use it 44 | 45 | chromecasts, browser = pychromecast.get_listed_chromecasts(friendly_names=item.addrs) 46 | if not chromecasts: 47 | return False 48 | for cast in chromecasts: 49 | cast.wait() 50 | #srv.logging.debug("cast=%r", cast) 51 | """ 52 | print('%r' % (cast.device.friendly_name,)) 53 | print('%r' % (cast.socket_client.host,)) 54 | """ 55 | mc = cast.media_controller 56 | mc.play_media(url, content_type=mime_type) 57 | mc.block_until_active() 58 | mc.play() # issue play, return immediately 59 | # TODO detect when play failed 60 | # if one end point works, but another fails is that a success or a failure? 61 | # What is an end point does NOT show up in list? i.e. len(item.addrs) > len(chromecasts) 62 | 63 | return True 64 | 65 | -------------------------------------------------------------------------------- /mqttwarn/services/dbus.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Fabian Affolter ' 5 | __copyright__ = 'Copyright 2014 Fabian Affolter' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | import dbus 9 | 10 | 11 | def plugin(srv, item): 12 | """Send a message through dbus to the user's desktop.""" 13 | 14 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 15 | 16 | text = item.message 17 | summary = item.addrs[0] 18 | app_name = item.get('title', srv.SCRIPTNAME) 19 | replaces_id = 0 20 | service = 'org.freedesktop.Notifications' 21 | path = '/' + service.replace('.', '/') 22 | interface = service 23 | app_icon = '/usr/share/icons/gnome/32x32/places/network-server.png' 24 | expire_timeout = 1000 25 | actions = [] 26 | hints = [] 27 | 28 | try: 29 | srv.logging.debug("Sending message to %s..." % (item.target)) 30 | session_bus = dbus.SessionBus() 31 | obj = session_bus.get_object(service, path) 32 | interface = dbus.Interface(obj, interface) 33 | interface.Notify(app_name, replaces_id, app_icon, summary, text, 34 | actions, hints, expire_timeout) 35 | srv.logging.debug("Successfully sent message") 36 | except Exception as e: 37 | srv.logging.error("Error sending message to %s: %s" % (item.target, e)) 38 | return False 39 | 40 | return True 41 | -------------------------------------------------------------------------------- /mqttwarn/services/desktopnotify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Alexander Gräf ' 5 | __copyright__ = 'Copyright 2022 Alexander Gräf' 6 | __version__ = '1.0.0' 7 | __license__ = 'Eclipse Public License - v 2.0 - https://www.eclipse.org/legal/epl-2.0/' 8 | 9 | import json 10 | import typing as t 11 | 12 | from desktop_notifier import DesktopNotifier, Urgency, Button, ReplyField 13 | 14 | from mqttwarn.model import Service, ProcessorItem, Struct 15 | 16 | notify = DesktopNotifier() 17 | 18 | def is_json(msg: t.Union[str, bytes]) -> bool: 19 | try: 20 | json.loads(msg) 21 | except ValueError as e: 22 | return False 23 | return True 24 | 25 | def plugin(srv: Service, item: ProcessorItem): 26 | # Log 27 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 28 | 29 | # Load Config 30 | config = item.config 31 | 32 | # Play Sound ? 33 | playSound = True 34 | if isinstance(config, dict): 35 | playSound = config.get('sound', True) 36 | 37 | # Get Message 38 | message = item.message 39 | if message and is_json(message): 40 | data = json.loads(message) 41 | else: 42 | data = { 43 | "title" : item.get('title', item.topic), 44 | "message": message 45 | } 46 | 47 | srv.logging.debug("Sending desktop notification") 48 | try: 49 | # Synchronous Notification (allows no callbacks in OSX) 50 | # Asynchronous would require asyncio and require some changes to the plugin handler 51 | notify.send_sync(message=data['message'], title=data['title'],sound=playSound) 52 | 53 | except Exception as e: 54 | srv.logging.warning("Invoking desktop notifier failed: %s" % e) 55 | return False 56 | 57 | return True 58 | -------------------------------------------------------------------------------- /mqttwarn/services/dnsupdate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from builtins import str 5 | 6 | __author__ = 'Jan-Piet Mens ' 7 | __copyright__ = 'Copyright 2015 Jan-Piet Mens' 8 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 9 | 10 | import dns.update 11 | import dns.query 12 | import dns.tsigkeyring 13 | 14 | 15 | def plugin(srv, item): 16 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 17 | 18 | config = item.config 19 | 20 | dns_nameserver = config['dns_nameserver'] 21 | dns_keyname = config['dns_keyname'] 22 | dns_keyblob = config['dns_keyblob'] 23 | 24 | try: 25 | zone, domain, ttl, rrtype = item.addrs 26 | 27 | except: 28 | srv.logging.error("Incorrect target configuration for {0}/{1}".format(item.service, item.target)) 29 | return False 30 | 31 | text = item.message 32 | 33 | try: 34 | keyring = dns.tsigkeyring.from_text({str(dns_keyname): str(dns_keyblob)}); 35 | 36 | update = dns.update.Update(zone, 37 | keyring=keyring, 38 | keyname=dns_keyname, 39 | keyalgorithm='hmac-sha256') # FIXME configurable 40 | 41 | if rrtype.upper() == 'TXT': 42 | text = '"%s"' % text 43 | 44 | update.replace(domain, ttl, rrtype, text) 45 | response = dns.query.tcp(update, dns_nameserver, timeout=10) 46 | 47 | srv.logging.debug("DNS Update: {0}".format(dns.rcode.to_text(response.rcode()))) 48 | except Exception as e: 49 | srv.logging.warning("Cannot notify to dnsupdate `%s': %s" % (dns_nameserver, e)) 50 | return False 51 | 52 | return True 53 | -------------------------------------------------------------------------------- /mqttwarn/services/emoncms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Ben Jones ' 5 | __copyright__ = 'Copyright 2014 Ben Jones' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | from future import standard_library 9 | standard_library.install_aliases() 10 | from builtins import str 11 | 12 | import urllib.request, urllib.parse, urllib.error 13 | 14 | try: 15 | import simplejson as json 16 | except ImportError: 17 | import json # type: ignore[no-redef] 18 | 19 | 20 | def plugin(srv, item): 21 | """ addrs: (node, name) """ 22 | 23 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 24 | 25 | url = item.config['url'] 26 | apikey = item.config['apikey'] 27 | timeout = item.config['timeout'] 28 | 29 | node = item.addrs[0] 30 | name = item.addrs[1] 31 | value = item.payload 32 | 33 | try: 34 | params = { 'apikey': apikey, 'node': node, 'json': json.dumps({ name : value }) } 35 | resource = url + '/input/post.json?' + urllib.parse.urlencode(params) 36 | 37 | request = urllib.request.Request(resource) 38 | request.add_header('User-agent', srv.SCRIPTNAME) 39 | 40 | response = urllib.request.urlopen(request, timeout=timeout) 41 | data = response.read() 42 | except Exception as e: 43 | srv.logging.warn("Failed to send GET request to EmonCMS using %s: %s" % (resource, e)) 44 | return False 45 | 46 | return True 47 | -------------------------------------------------------------------------------- /mqttwarn/services/execute.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __author__ = 'Tobias Brunner ' 4 | __copyright__ = 'Copyright 2016 Tobias Brunner' 5 | __license__ = """Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)""" 6 | 7 | import subprocess 8 | 9 | def plugin(srv, item): 10 | 11 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 12 | 13 | config = item.config 14 | if type(config) == dict and 'text_replace' in config: 15 | replace = config['text_replace'] 16 | else: 17 | replace = '[TEXT]' 18 | 19 | text = item.message 20 | cmd = [i.replace(replace, text) for i in item.addrs] 21 | 22 | try: 23 | subprocess.check_call(cmd, stdin=None, stderr=subprocess.STDOUT, shell=False, universal_newlines=True, cwd='/tmp') 24 | except Exception as e: 25 | srv.logging.warning("Cannot execute %s because %s" % (cmd, e)) 26 | return False 27 | 28 | return True 29 | -------------------------------------------------------------------------------- /mqttwarn/services/fbchat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Przemek Anuszek ' 5 | __copyright__ = 'Copyright 2016 Przemek Anuszek' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | from fbchat import Client 9 | from fbchat.models import * 10 | 11 | 12 | def plugin(srv, item): 13 | 14 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 15 | 16 | client = item.addrs[0] 17 | password = item.addrs[1] 18 | friend = item.addrs[2] 19 | 20 | fbclient = Client(client, password) 21 | friends = fbclient.searchForUsers(friend) 22 | ffriend = friends[0] 23 | 24 | srv.logging.debug("user %s" % (item.target)) 25 | 26 | text = item.message 27 | try: 28 | srv.logging.debug("Sending msg to %s..." % (item.target)) 29 | sent = fbclient.sendMessage(text, thread_id=ffriend.uid, thread_type=ThreadType.USER) 30 | srv.logging.debug("Successfully sent message") 31 | except Exception as e: 32 | srv.logging.error("Error sending fbchat to %s: %s" % (item.target, e)) 33 | return False 34 | client.logout() 35 | return True 36 | -------------------------------------------------------------------------------- /mqttwarn/services/file.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Jan-Piet Mens ' 5 | __copyright__ = 'Copyright 2014 Jan-Piet Mens' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | import io 9 | import tempfile 10 | 11 | 12 | def plugin(srv, item): 13 | 14 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 15 | 16 | mode = "a" 17 | 18 | # item.config is brought in from the configuration file 19 | config = item.config 20 | 21 | # Evaluate global parameters. 22 | newline = False 23 | overwrite = False 24 | if type(config) == dict and 'append_newline' in config and config['append_newline']: 25 | newline = True 26 | if type(config) == dict and 'overwrite' in config and config['overwrite']: 27 | overwrite = True 28 | 29 | # `item.addrs` is either a dict or a list associated with a particular target. 30 | # While lists may contain more than one item (e.g., for the pushover target), 31 | # the `file` service only allows for single items, the path name. 32 | # When it's a dict, additional parameters can be obtained to augment the 33 | # behavior of the write operation on a per-file basis. 34 | if isinstance(item.addrs, dict): 35 | filename = item.addrs['path'].format(**item.data) 36 | # Evaluate per-file parameters. 37 | newline = item.addrs.get('append_newline', newline) 38 | overwrite = item.addrs.get('overwrite', overwrite) 39 | else: 40 | filename = item.addrs[0].format(**item.data) 41 | 42 | # Interpolate some variables into filename. 43 | if "$TMPDIR" in filename: 44 | filename = filename.replace("$TMPDIR", tempfile.gettempdir()) 45 | 46 | srv.logging.info("Writing to file `%s'" % (filename)) 47 | 48 | # If the incoming payload has been transformed, use that, 49 | # else the original payload 50 | text = item.message 51 | 52 | if newline: 53 | text += "\n" 54 | if overwrite: 55 | mode = "w" 56 | 57 | if isinstance(text, bytes): 58 | mode += "b" 59 | encoding = None 60 | else: 61 | encoding = "utf-8" 62 | 63 | try: 64 | f = io.open(filename, mode=mode, encoding=encoding) 65 | f.write(text) 66 | f.close() 67 | 68 | except Exception as e: 69 | srv.logging.error("Cannot write to file `%s': %s" % (filename, e)) 70 | return False 71 | 72 | return True 73 | -------------------------------------------------------------------------------- /mqttwarn/services/freeswitch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Ben Jones ' 5 | __copyright__ = 'Copyright 2014 Ben Jones' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | from future import standard_library 9 | standard_library.install_aliases() 10 | 11 | from xmlrpc.client import ServerProxy 12 | import urllib.request, urllib.parse, urllib.error 13 | 14 | 15 | def plugin(srv, item): 16 | 17 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 18 | 19 | host = item.config['host'] 20 | port = item.config['port'] 21 | username = item.config['username'] 22 | password = item.config['password'] 23 | ttsurl = item.config['ttsurl'] 24 | ttsparams = item.config['ttsparams'] 25 | 26 | gateway = item.addrs[0] 27 | number = item.addrs[1] 28 | title = item.title 29 | 30 | if ttsurl.startswith('http://'): 31 | ttsurl = ttsurl[7:] 32 | elif ttsurl.startswith('https://'): 33 | ttsurl = ttsurl[8:] 34 | 35 | if ttsparams is not None: 36 | for key in list(ttsparams.keys()): 37 | 38 | # { 'q' : '@message' } 39 | # Quoted field, starts with '@'. Do not use .format, instead grab 40 | # the item's [message] and inject as parameter value. 41 | if ttsparams[key].startswith('@'): # "@message" 42 | ttsparams[key] = item.get(ttsparams[key][1:], "NOP") 43 | 44 | else: 45 | try: 46 | ttsparams[key] = ttsparams[key].format(**item.data).encode('utf-8') 47 | except Exception as e: 48 | srv.logging.debug("Parameter %s cannot be formatted: %s" % (key, e)) 49 | return False 50 | 51 | try: 52 | # TTS service 53 | shout_url = "shout://%s" % ttsurl 54 | if ttsparams is not None: 55 | if not shout_url.endswith('?'): 56 | shout_url = shout_url + '?' 57 | shout_url = shout_url + urllib.parse.urlencode(ttsparams) 58 | # debugging 59 | srv.logging.debug("Shout URL: %s" % shout_url) 60 | # Freeswitch API 61 | server = ServerProxy("http://%s:%s@%s:%d" % (username, password, host, port)) 62 | # channel variables we need to setup the call 63 | channel_vars = "{ignore_early_media=true,originate_timeout=60,origination_caller_id_name='" + title + "'}" 64 | # originate the call 65 | server.freeswitch.api("originate", channel_vars + gateway + number + " &playback(" + shout_url + ")") 66 | except Exception as e: 67 | srv.logging.error("Error originating Freeswitch VOIP call to %s via %s%s: %s" % (item.target, gateway, number, e)) 68 | return False 69 | 70 | return True 71 | -------------------------------------------------------------------------------- /mqttwarn/services/hangbot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | __author__ = 'Bram Hendrickx' 5 | __copyright__ = 'Copyright 2016 Bram Hendrickx' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | Original script by Bram Hendrickx. https://github.com/mqtt-tools/mqttwarn/blob/main/mqttwarn/services/ifttt.py 9 | Modified to work with hangoutsbot api plugin https://github.com/hangoutsbot/hangoutsbot/wiki/API-Plugin 10 | """ 11 | 12 | __author__ = 'Michael Brougham' 13 | __copyright__ = 'Copyright 2018 Michael Brougham' 14 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 15 | 16 | 17 | import json 18 | import requests 19 | 20 | 21 | def plugin(srv, item): 22 | """ expects (url,port,apikey, convid) in addrs """ 23 | 24 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 25 | 26 | payload = { 27 | 'key': item.addrs[2], 28 | 'sendto': item.addrs[3], 29 | 'content': item.message 30 | } 31 | try: 32 | srv.logging.debug("Sending to hangoutsbot") 33 | url = "https://" + item.addrs[0] + ":" + item.addrs[1] 34 | headers = {'content-type': 'application/json'} 35 | requests.post(url, data = json.dumps(payload), headers = headers, verify=False) 36 | srv.logging.debug("Successfully sent to hangoutsbot") 37 | except Exception as e: 38 | srv.logging.warning("Failed to send message to hangoutsbot" % e) 39 | return False 40 | 41 | return True 42 | -------------------------------------------------------------------------------- /mqttwarn/services/icinga2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Ben Jones ' 5 | __copyright__ = 'Copyright 2016 Ben Jones' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | import requests 9 | from requests.auth import HTTPBasicAuth 10 | 11 | try: 12 | import simplejson as json 13 | except ImportError: 14 | import json # type: ignore[no-redef] 15 | 16 | 17 | def plugin(srv, item): 18 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 19 | 20 | host = item.config['host'] 21 | port = item.config['port'] 22 | username = item.config['username'] 23 | password = item.config['password'] 24 | 25 | # optional ca-cert (usually self-signed cert installed by icinga2) 26 | if 'cacert' in item.config: 27 | cacert = item.config['cacert'] 28 | else: 29 | cacert = None 30 | 31 | # e.g. example.com!ping4 32 | check_host = item.addrs[0] 33 | check_service = item.addrs[1] 34 | check_source = item.addrs[2] 35 | 36 | if check_service is None: 37 | check_type = 'host' 38 | check_target = check_host 39 | else: 40 | check_type = 'service' 41 | check_target = '{0}!{1}'.format(check_host, check_service) 42 | 43 | if check_source is None: 44 | check_source = 'mqttwarn' 45 | 46 | payload = { 47 | 'exit_status': item.priority, 48 | 'plugin_output': item.message, 49 | 'check_source': check_source, 50 | check_type: check_target, 51 | } 52 | 53 | # Update our payload with any JSON data in the message. 54 | try: 55 | payload.update(json.loads(item.message)) 56 | except Exception: 57 | pass 58 | 59 | # Request parameters 60 | headers = { 61 | "Accept": "application/json" 62 | } 63 | 64 | kwargs = { 65 | "headers": headers, 66 | "auth": HTTPBasicAuth(username, password), 67 | "json": payload 68 | } 69 | 70 | if cacert: 71 | kwargs["verify"] = cacert 72 | 73 | try: 74 | url = "%s:%d/v1/actions/process-check-result" % (host, port) 75 | r = requests.post(url, **kwargs) 76 | if r.status_code != requests.codes.ok: 77 | srv.logging.warning("Invalid response from icinga2 REST API at `%s`: %s" % (host, r.text)) 78 | return False 79 | except Exception as e: 80 | srv.logging.warning("Failed to POST request to icinga2 REST API at `%s': %s" % (host, e)) 81 | return False 82 | 83 | return True 84 | -------------------------------------------------------------------------------- /mqttwarn/services/ifttt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Bram Hendrickx' 5 | __copyright__ = 'Copyright 2016 Bram Hendrickx' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | import requests 9 | 10 | 11 | def plugin(srv, item): 12 | ''' expects (apikey, event) in adddrs ''' 13 | 14 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 15 | 16 | event_type = "device_iden" 17 | try: 18 | apikey, event = item.addrs 19 | except: 20 | try: 21 | apikey, event = item.addrs 22 | except: 23 | srv.logging.warn("ifttt target is incorrectly configured") 24 | return False 25 | 26 | payload = {} 27 | payload["value1"] = item.message 28 | 29 | try: 30 | srv.logging.debug("Sending ifttt event") 31 | url = "https://maker.ifttt.com/trigger/" + event + "/with/key/" + apikey 32 | requests.post(url, data=payload) 33 | srv.logging.debug("Successfully sent ifttt event") 34 | except Exception as e: 35 | srv.logging.warning("Cannot send ifttt event: %s" % e) 36 | return False 37 | 38 | return True 39 | -------------------------------------------------------------------------------- /mqttwarn/services/irccat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Jan-Piet Mens ' 5 | __copyright__ = 'Copyright 2014 Jan-Piet Mens' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | import socket 9 | 10 | 11 | def plugin(srv, item): 12 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 13 | 14 | try: 15 | addr, port, channel = item.addrs 16 | except: 17 | srv.logging.warning("Incorrect target configuration") 18 | return False 19 | 20 | message = item.message 21 | 22 | # Optionally apply coloring. 23 | color = None 24 | if item.priority == 1: 25 | color = '%GREEN' 26 | elif item.priority == 2: 27 | color = '%RED' 28 | if color is not None: 29 | message = color + message 30 | 31 | srv.logging.debug("Sending to IRCcat: %s" % (message)) 32 | 33 | # Apparently, a trailing newline is needed. 34 | # https://github.com/mqtt-tools/mqttwarn/issues/547#issuecomment-944632712 35 | message += "\n" 36 | 37 | try: 38 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 39 | sock.connect((addr, port)) 40 | sock.send(message.encode()) 41 | sock.close() 42 | 43 | except Exception as e: 44 | srv.logging.error("Error sending IRCcat notification to %s:%s [%s]: %s" % (item.target, addr, port, e)) 45 | return False 46 | 47 | return True 48 | -------------------------------------------------------------------------------- /mqttwarn/services/linuxnotify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Fabian Affolter ' 5 | __copyright__ = 'Copyright 2014 Fabian Affolter' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | from gi.repository import Notify 9 | 10 | 11 | def plugin(srv, item): 12 | """Send a message to the user's desktop notification system.""" 13 | 14 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, 15 | item.service, item.target) 16 | 17 | title = item.addrs[0] 18 | text = item.message 19 | 20 | try: 21 | srv.logging.debug("Sending notification to the user's desktop") 22 | Notify.init('mqttwarn') 23 | n = Notify.Notification.new( 24 | title, 25 | text, 26 | '/usr/share/icons/gnome/32x32/places/network-server.png') 27 | n.show() 28 | srv.logging.debug("Successfully sent notification") 29 | except Exception as e: 30 | srv.logging.warning("Cannot invoke notification to linux: %s" % e) 31 | return False 32 | 33 | return True 34 | -------------------------------------------------------------------------------- /mqttwarn/services/log.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Jan-Piet Mens ' 5 | __copyright__ = 'Copyright 2014 Jan-Piet Mens' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | 9 | def plugin(srv, item): 10 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 11 | 12 | assert isinstance(item.addrs, list), "`item.addrs` is not a list" 13 | 14 | level = item.addrs[0] 15 | 16 | text = item.message 17 | 18 | levels = { 19 | 'debug': srv.logging.debug, 20 | 'info': srv.logging.info, 21 | 'warn': srv.logging.warning, 22 | 'crit': srv.logging.critical, 23 | 'error': srv.logging.error, 24 | } 25 | 26 | try: 27 | levels[level]("%s", text) 28 | except Exception as e: 29 | srv.logging.error("Cannot invoke service log with level `%s': %s" % (level, e)) 30 | return False 31 | 32 | return True 33 | -------------------------------------------------------------------------------- /mqttwarn/services/mattermost.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Jan-Piet Mens ' 5 | __copyright__ = 'Copyright 2018 Jan-Piet Mens' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | from six import string_types 9 | 10 | import requests 11 | 12 | try: 13 | import simplejson as json 14 | except ImportError: 15 | import json # type: ignore[no-redef] 16 | 17 | 18 | def plugin(srv, item): 19 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 20 | 21 | hook_url = item.addrs[0] 22 | channel = item.addrs[1] 23 | username = item.addrs[2] # may be None 24 | icon_url = item.addrs[3] # may be None 25 | 26 | text = item.message 27 | try: 28 | """ Try to format a Markdown table if we have JSON in the payload """ 29 | """ ETOOMESSY; volunteers to refactor? """ 30 | title = item.get('title', None) 31 | j = json.loads(text) 32 | keylen = vallen = 10 33 | for key in j: 34 | # print type(key), keylen, len(key) 35 | if isinstance(key, string_types) and keylen < len(key): 36 | keylen = len(key) 37 | if isinstance(j[key], string_types) and vallen < len(j[key]): 38 | vallen = len(j[key]) 39 | s = "" 40 | if title is not None and title != "": 41 | s = "## %s\n" % title 42 | key = "key" 43 | val = "value" 44 | s = s + "| {0:<{kw}} | {1:<{vw}} |\n".format("key", "value", kw=keylen, vw=vallen) 45 | s = s + "|:{0:<{kw}} |:{1:<{vw}} |\n".format('-' * keylen, '-' * vallen, kw=keylen, vw=vallen) 46 | for key in j: 47 | s = s + "| {0:<{kw}} | {1:<{vw}} |\n".format(key, j[key], kw=keylen, vw=vallen) 48 | text = s 49 | except Exception as e: 50 | srv.logging.debug("not JSON; proceeding with text") 51 | pass 52 | 53 | payload = {} 54 | payload["channel"] = channel 55 | payload["text"] = text 56 | if username is not None: 57 | payload["username"] = username 58 | if icon_url is not None: 59 | payload["icon_url"] = icon_url 60 | 61 | # print payload 62 | 63 | headers = { 64 | "Content-type": "application/json", 65 | "Accept": "application/json" 66 | } 67 | 68 | try: 69 | r = requests.post(hook_url, data=json.dumps(payload), headers=headers) 70 | if r.status_code != requests.codes.ok: 71 | srv.logging.warning("Invalid response from Mattermost Webhook: %s" % (r.text)) 72 | return False 73 | except Exception as e: 74 | srv.logging.warning("Failed to POST request to Mattermost Webhook: %s" % e) 75 | return False 76 | 77 | return True 78 | -------------------------------------------------------------------------------- /mqttwarn/services/mqtt_filter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Joerg Gollnick ' 5 | __copyright__ = 'Copyright 2016 Tobias Brunner / 2021 Joerg Gollnick' 6 | __license__ = """Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)""" 7 | 8 | import subprocess 9 | import json 10 | from pipes import quote 11 | from six import string_types 12 | 13 | def plugin(srv, item): 14 | 15 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 16 | 17 | # same as for ssh 18 | json_message=json.loads(item.message) 19 | 20 | args = None 21 | if json_message is not None: 22 | args = json_message["args"] 23 | 24 | if type(args) is list and len(args) == 1: 25 | args=args[0] 26 | 27 | if type(args) is list: 28 | args=tuple([ quote(v) for v in args ]) #escape the shell args 29 | elif type(args) is str or type(args) is unicode: 30 | args=(quote(args),) 31 | 32 | # parse topic 33 | topic=list(map( lambda x: quote(x), item.topic.split('/') )) 34 | 35 | outgoing_topic = None 36 | # replace palceholders args[0], args[1] ..., full_topic, topic[0], 37 | if item.addrs[0] is not None: 38 | outgoing_topic = item.addrs[0].format(args=args,full_topic=quote(item.topic),topic=topic) 39 | qos = item.addrs[1] 40 | retain = item.addrs[2] 41 | addrs = item.addrs[3:] 42 | 43 | cmd = None 44 | if addrs is not None: 45 | cmd = [i.format(args=args, full_topic=quote(item.topic),topic=topic) for i in addrs] 46 | 47 | srv.logging.debug("*** MODULE=%s: service=%s, command=%s outgoing_topic=%s", __file__, item.service, str( cmd ),outgoing_topic) 48 | 49 | try: 50 | res = subprocess.check_output(cmd, stdin=None, stderr=subprocess.STDOUT, shell=False, universal_newlines=True, cwd='/tmp') 51 | except Exception as e: 52 | srv.logging.warning("Cannot execute %s because %s" % (cmd, e)) 53 | return False 54 | 55 | if outgoing_topic is not None: 56 | outgoing_payload = res.rstrip('\n') 57 | if isinstance(outgoing_payload, string_types): 58 | outgoing_payload = bytearray(outgoing_payload, encoding='utf-8') 59 | try: 60 | srv.mqttc.publish(outgoing_topic, outgoing_payload, qos=qos, retain=retain) 61 | except Exception as e: 62 | srv.logging.warning("Cannot PUBlish response %s: %s" % (outgoing_topic, e)) 63 | return False 64 | 65 | return True 66 | -------------------------------------------------------------------------------- /mqttwarn/services/mqttpub.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from six import string_types 5 | 6 | __author__ = 'Jan-Piet Mens ' 7 | __copyright__ = 'Copyright 2014 Jan-Piet Mens' 8 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 9 | 10 | 11 | def plugin(srv, item): 12 | """ Publish via MQTT to the same broker connection. 13 | Requires topic, qos and retain flag """ 14 | 15 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 16 | 17 | outgoing_topic = item.addrs[0] 18 | qos = item.addrs[1] 19 | retain = item.addrs[2] 20 | 21 | # Attempt to interpolate data into topic name. If it isn't possible 22 | # ignore, and return without publish 23 | 24 | if item.data is not None: 25 | try: 26 | outgoing_topic = item.addrs[0].format(**item.data) 27 | except: 28 | srv.logging.debug("Outgoing topic cannot be formatted; not published") 29 | return False 30 | 31 | outgoing_payload = item.message 32 | if isinstance(outgoing_payload, string_types): 33 | outgoing_payload = bytearray(outgoing_payload, encoding='utf-8') 34 | 35 | try: 36 | srv.mqttc.publish(outgoing_topic, outgoing_payload, qos=qos, retain=retain) 37 | except Exception as e: 38 | srv.logging.warning("Cannot PUBlish via `mqttpub:%s': %s" % (item.target, e)) 39 | return False 40 | 41 | return True 42 | -------------------------------------------------------------------------------- /mqttwarn/services/mythtv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Stefan Roellin ' 5 | __copyright__ = 'Copyright 2015 Stefan Roellin' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | from future import standard_library 9 | standard_library.install_aliases() 10 | 11 | import http.client, urllib.request, urllib.parse, urllib.error 12 | 13 | 14 | def plugin(srv, item): 15 | 16 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 17 | 18 | data = {'Message': item.message.encode('utf-8'), 19 | 'Origin': item.title, 20 | 'Timeout': item.config.get('timeout', 5), 21 | 'Address': item.addrs[1], 22 | 'Progress': -1 23 | } 24 | if item.image != None: 25 | data['Image'] = item.image 26 | 27 | http_handler = http.client.HTTPConnection(item.addrs[0]) 28 | 29 | try: 30 | http_handler.request("POST", 'Myth/SendNotification', 31 | headers={'Content-type': "application/x-www-form-urlencoded", "Accept": "text/plain"}, 32 | body=urllib.parse.urlencode(data)) 33 | except (SSLError, HTTPException) as e: 34 | srv.logging.warn("mythtv notification failed: %s" % e) 35 | return False 36 | 37 | response = http_handler.getresponse() 38 | 39 | srv.logging.debug("Reponse: %s, %s" % (response.status, response.reason)) 40 | 41 | return True 42 | -------------------------------------------------------------------------------- /mqttwarn/services/nntp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Jan-Piet Mens ' 5 | __copyright__ = 'Copyright 2014 Jan-Piet Mens' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | import nntplib 9 | from six import StringIO 10 | from email.mime.text import MIMEText 11 | from email.utils import formatdate 12 | 13 | 14 | def plugin(srv, item): 15 | 16 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 17 | 18 | host = item.config.get('server', 'localhost') 19 | port = item.config.get('port', 119) 20 | username = item.config.get('username') 21 | password = item.config.get('password') 22 | 23 | try: 24 | from_hdr = item.addrs[0] 25 | newsgroup = item.addrs[1] 26 | except Exception: 27 | srv.logging.error("Incorrect target configuration for %s" % item.target) 28 | return False 29 | 30 | try: 31 | 32 | text = item.message 33 | title = item.title 34 | 35 | msg = MIMEText(text) 36 | 37 | msg['From'] = from_hdr 38 | msg['Subject'] = item.title 39 | msg['Newsgroups'] = newsgroup 40 | msg['Date'] = formatdate() 41 | msg['User-Agent'] = srv.SCRIPTNAME 42 | # msg['Message-ID'] = '' 43 | 44 | msg_file = StringIO(msg.as_string()) 45 | nntp = nntplib.NNTP(host, port, user=username, password=password) 46 | 47 | srv.logging.debug(nntp.getwelcome()) 48 | nntp.set_debuglevel(0) 49 | 50 | nntp.post(msg_file) 51 | nntp.quit() 52 | except Exception as e: 53 | srv.logging.warn("Cannot post to %s newsgroup: %s" % (newsgroup, e)) 54 | return False 55 | 56 | return True 57 | -------------------------------------------------------------------------------- /mqttwarn/services/noop.py: -------------------------------------------------------------------------------- 1 | from mqttwarn.model import Service, ProcessorItem 2 | 3 | 4 | def plugin(srv: Service, item: ProcessorItem) -> bool: 5 | """ 6 | An mqttwarn plugin with little overhead, suitable for unit- and integration-testing. 7 | """ 8 | if hasattr(item, "message") and item.message and "fail" in item.message: 9 | srv.logging.error("Failed sending message using noop") 10 | return False 11 | else: 12 | srv.logging.info("Successfully sent message using noop") 13 | return True 14 | -------------------------------------------------------------------------------- /mqttwarn/services/nsca.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Jan-Piet Mens ' 5 | __copyright__ = 'Copyright 2014 Jan-Piet Mens' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | 9 | import pynsca 10 | from pynsca import NSCANotifier 11 | 12 | 13 | def plugin(srv, item): 14 | 15 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 16 | 17 | config = item.config 18 | 19 | statii = [ pynsca.OK, pynsca.WARNING, pynsca.CRITICAL, pynsca.UNKNOWN ] 20 | status = pynsca.OK 21 | try: 22 | prio = item.priority 23 | status = statii[prio] 24 | except: 25 | pass 26 | 27 | nsca_host = str(config['nsca_host']) 28 | 29 | host_name = item.addrs[0] 30 | service_description = item.addrs[1] 31 | 32 | # If the incoming payload has been transformed, use that, 33 | # else the original payload 34 | text = item.message 35 | 36 | try: 37 | notif = NSCANotifier(nsca_host) 38 | notif.svc_result(host_name, service_description, status, text) 39 | except Exception as e: 40 | srv.logging.warning("Cannot notify to NSCA host `%s': %s" % (nsca_host, e)) 41 | return False 42 | 43 | return True 44 | -------------------------------------------------------------------------------- /mqttwarn/services/osxsay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Jan-Piet Mens ' 5 | __copyright__ = 'Copyright 2014 Jan-Piet Mens' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | import subprocess 9 | 10 | 11 | def plugin(srv, item): 12 | 13 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 14 | 15 | voice = item.addrs[0] 16 | text = item.message 17 | 18 | argv = [ "/usr/bin/say", "-f", "-", "--voice=%s" % voice ] 19 | 20 | try: 21 | proc = subprocess.Popen(argv, 22 | stdin=subprocess.PIPE, close_fds=True) 23 | except Exception as e: 24 | srv.logging.warn("Cannot create osxsay pipe: %s" % e) 25 | return False 26 | 27 | try: 28 | proc.stdin.write(text) 29 | except IOError as e: 30 | srv.logging.warn("Cannot write to osxsay pipe: errno %d" % (e.errno)) 31 | return False 32 | except Exception as e: 33 | srv.logging.warn("Cannot write to osxsay pipe: %s" % e) 34 | return False 35 | 36 | proc.stdin.close() 37 | proc.wait() 38 | return True 39 | -------------------------------------------------------------------------------- /mqttwarn/services/pastebinpub.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Fabian Affolter ' 5 | __copyright__ = 'Copyright 2014 Fabian Affolter' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | from pastebin import PastebinAPI 9 | 10 | 11 | def plugin(srv, item): 12 | """ Pushlish the message to pastebin.com """ 13 | 14 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, 15 | item.service, item.target) 16 | 17 | pastebin_data = item.addrs 18 | 19 | pastebinapi = PastebinAPI() 20 | api_dev_key = pastebin_data[0] 21 | username = pastebin_data[1] 22 | password = pastebin_data[2] 23 | pastename = 'mqttwarn' 24 | pasteprivate = pastebin_data[3] 25 | expiredate = pastebin_data[4] 26 | 27 | text = item.message 28 | 29 | try: 30 | api_user_key = pastebinapi.generate_user_key( 31 | api_dev_key, 32 | username, 33 | password) 34 | except Exception as e: 35 | srv.logging.warn("Cannot retrieve session data from pastebin: %s" % e) 36 | return False 37 | 38 | try: 39 | srv.logging.debug("Adding entry to pastebin.com as user %s..." % (username)) 40 | pastebinapi.paste( 41 | api_dev_key, 42 | text, 43 | api_user_key = api_user_key, 44 | paste_name = pastename, 45 | paste_format = None, 46 | paste_private = pasteprivate, 47 | paste_expire_date = expiredate 48 | ) 49 | srv.logging.debug("Successfully added paste to pastebin") 50 | except Exception as e: 51 | srv.logging.warn("Cannot publish to pastebin: %s" % e) 52 | return False 53 | 54 | return True 55 | -------------------------------------------------------------------------------- /mqttwarn/services/pipe.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Jan-Piet Mens ' 5 | __copyright__ = 'Copyright 2014 Jan-Piet Mens' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | import errno 9 | import subprocess 10 | 11 | 12 | def plugin(srv, item): 13 | 14 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 15 | 16 | # addrs is a list[] with process name and args 17 | argv = item.addrs 18 | 19 | text = item.message 20 | 21 | if not text.endswith("\n"): 22 | text = text + "\n" 23 | 24 | try: 25 | proc = subprocess.Popen(argv, 26 | stdin=subprocess.PIPE, close_fds=True) 27 | except Exception as e: 28 | srv.logging.warn("Cannot create pipe: %s" % e) 29 | return False 30 | 31 | try: 32 | proc.stdin.write(text.encode('utf-8')) 33 | except IOError as e: 34 | srv.logging.warn("Cannot write to pipe: errno %d" % (e.errno)) 35 | return False 36 | except Exception as e: 37 | srv.logging.warn("Cannot write to pipe: %s" % e) 38 | return False 39 | 40 | proc.stdin.close() 41 | proc.wait() 42 | return True 43 | -------------------------------------------------------------------------------- /mqttwarn/services/prowl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Jan-Piet Mens ' 5 | __copyright__ = 'Copyright 2014-2021 Jan-Piet Mens' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | import pyprowl 9 | 10 | 11 | def plugin(srv, item): 12 | 13 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 14 | 15 | apikey = item.addrs[0] 16 | application = item.addrs[1] 17 | 18 | title = item.get('title', srv.SCRIPTNAME) 19 | text = item.message 20 | priority = int(item.get('priority', 0)) 21 | 22 | try: 23 | p = pyprowl.Prowl(apikey) 24 | p.verify_key() 25 | srv.logging.info("Prowl API key successfully verified") 26 | except Exception as e: 27 | srv.logging.error("Error verifying Prowl API key: {}".format(e)) 28 | return False 29 | 30 | try: 31 | p.notify(event=title, description=text, 32 | priority=priority, url=None, 33 | appName=application) 34 | srv.logging.debug("Sending notification to Prowl succeeded") 35 | except Exception as e: 36 | srv.logging.warning("Sending notification to Prowl failed: %s" % e) 37 | return False 38 | 39 | return True 40 | -------------------------------------------------------------------------------- /mqttwarn/services/redispub.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Jan-Piet Mens ' 5 | __copyright__ = 'Copyright 2014 Jan-Piet Mens' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | import redis 9 | 10 | 11 | def plugin(srv, item): 12 | """ redispub. Expects addrs to contain (channel) """ 13 | 14 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 15 | 16 | host = item.config.get('host', 'localhost') 17 | port = int(item.config.get('port', 6379)) 18 | 19 | try: 20 | rp = redis.Redis(host, port) 21 | except Exception as e: 22 | srv.logging.warn("Cannot connect to redis on %s:%s : %s" % (host, port, e)) 23 | return False 24 | 25 | channel = item.addrs[0] 26 | text = item.message 27 | 28 | try: 29 | rp.publish(channel, text) 30 | except Exception as e: 31 | srv.logging.warn("Cannot publish to redis on %s:%s : %s" % (host, port, e)) 32 | return False 33 | 34 | return True 35 | -------------------------------------------------------------------------------- /mqttwarn/services/rrdtool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'devsaurus ' 5 | __copyright__ = 'Copyright 2015' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | import re 9 | import rrdtool 10 | 11 | 12 | def plugin(srv, item): 13 | 14 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 15 | 16 | # If the incoming payload has been transformed, use that, 17 | # else the original payload 18 | text = item.message 19 | 20 | try: 21 | # addrs is a list[] associated with a particular target. 22 | # it can contain an arbitrary amount of entries that are just 23 | # passed along to rrdtool 24 | # mofified by otfdr @ github to accept abitray arguments with 25 | # the payload and to not always add the 'N' in front 26 | # 2017-06-05 - fix/enhancement for https://github.com/mqtt-tools/mqttwarn/issues/248 27 | if re.match( "^\d+$", text ): 28 | rrdtool.update(item.addrs, "N:" + text) 29 | else: 30 | rrdtool.update(item.addrs + text.split()) 31 | except Exception as e: 32 | srv.logging.warning("Cannot call rrdtool") 33 | return False 34 | 35 | return True 36 | -------------------------------------------------------------------------------- /mqttwarn/services/serial.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Daniel Lindner ' 5 | __copyright__ = 'Copyright 2016 Daniel Lindner' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | import serial 9 | from builtins import bytes 10 | 11 | _serialport = None 12 | 13 | 14 | def plugin(srv, item): 15 | global _serialport 16 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 17 | 18 | # item.config is brought in from the configuration file 19 | config = item.config 20 | 21 | # addrs is a list[] associated with a particular target. 22 | # While it may contain more than one item (e.g. pushover) 23 | # the `serial' service carries one two, i.e. a com name and baudrate 24 | try: 25 | comName = item.addrs[0].format(**item.data) 26 | comBaudRate = int(item.addrs[1]) 27 | except: 28 | srv.logging.error("Incorrect target configuration for {0}/{1}".format(item.service, item.target)) 29 | return False 30 | 31 | # If the incoming payload has been transformed, use that, 32 | # else the original payload 33 | text = item.message 34 | 35 | # If message specifies the hex keyword try to transform bytes from hex 36 | # else send string as it is 37 | test = text[:5] 38 | if test == ":HEX:": 39 | text = bytes(bytearray.fromhex(text[5:])) 40 | 41 | # Append newline if config option is set 42 | if type(config) == dict and 'append_newline' in config and config['append_newline']: 43 | text = text + "\n" 44 | 45 | try: 46 | try: 47 | if callable(getattr(_serialport, "is_open", None)): 48 | _serialport.is_open 49 | else: 50 | _serialport.isOpen 51 | srv.logging.debug("%s already open", comName) 52 | except: 53 | #Open port for first use 54 | srv.logging.debug("Open %s with %d baud", comName, comBaudRate) 55 | _serialport = serial.serial_for_url(comName) 56 | _serialport.baudrate = comBaudRate 57 | 58 | _serialport.write(text.encode('utf-8')) 59 | 60 | except serial.SerialException as e: 61 | srv.logging.warning("Cannot write to com port `%s': %s" % (comName, e)) 62 | return False 63 | 64 | return True 65 | 66 | -------------------------------------------------------------------------------- /mqttwarn/services/slixmpp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Based on xmpp plugin 5 | __originalauthor__ = 'Fabian Affolter ' 6 | __author__ = 'Remi Vincent ' 7 | __copyright__ = 'Copyright 2020 Remi Vincent' 8 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 9 | 10 | import slixmpp 11 | import asyncio 12 | 13 | class send_msg_bot(slixmpp.ClientXMPP): 14 | def __init__(self, sender, password, recipient, message, loop): 15 | self.loop = loop 16 | asyncio.set_event_loop(loop) 17 | slixmpp.ClientXMPP.__init__(self, sender, password) 18 | self.recipient = recipient 19 | self.message = message 20 | self.add_event_handler("session_start", self.start) 21 | 22 | async def start(self, event): 23 | self.send_message(mto = self.recipient, mbody = self.message, mtype = 'chat') 24 | self.disconnect() 25 | 26 | def plugin(srv, item): 27 | """Send a message to XMPP recipient(s).""" 28 | 29 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 30 | 31 | xmpp_addresses = item.addrs 32 | sender = item.config['sender'] 33 | password = item.config['password'] 34 | text = item.message 35 | 36 | if not xmpp_addresses: 37 | srv.logging.warn("Skipped sending XMPP notification to %s, " 38 | "no addresses configured" % (item.target)) 39 | return False 40 | 41 | try: 42 | srv.logging.debug("Sending XMPP notification to %s, addresses: %s" % (item.target, xmpp_addresses)) 43 | loop = asyncio.new_event_loop() 44 | for target in xmpp_addresses: 45 | xmpp = send_msg_bot(sender, password, target, text, loop) 46 | xmpp.connect() 47 | xmpp.process(forever=False) 48 | srv.logging.debug("Successfully sent message") 49 | except Exception as e: 50 | srv.logging.error("Error sending message to %s: %s" % (item.target, e)) 51 | return False 52 | 53 | return True 54 | -------------------------------------------------------------------------------- /mqttwarn/services/smtp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Jan-Piet Mens ' 5 | __copyright__ = 'Copyright 2014 Jan-Piet Mens' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | from builtins import str 9 | import smtplib 10 | from email.mime.text import MIMEText 11 | from email.mime.multipart import MIMEMultipart 12 | 13 | 14 | def plugin(srv, item): 15 | """Send a message to SMTP recipient(s).""" 16 | 17 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 18 | 19 | smtp_addresses = item.addrs 20 | 21 | server = item.config['server'] 22 | sender = item.config['sender'] 23 | starttls = item.config.get('starttls') 24 | username = item.config.get('username') 25 | password = item.config.get('password') 26 | 27 | if item.config.get("htmlmsg"): 28 | msg = MIMEMultipart('alternative') 29 | msg.attach(MIMEText(item.message, 'plain')) 30 | msg.attach(MIMEText(item.message, 'html')) 31 | else: 32 | msg = MIMEText(item.message, 'plain') 33 | msg['Subject'] = item.get('title', "%s notification" % (srv.SCRIPTNAME)) 34 | msg['To'] = ", ".join(smtp_addresses) 35 | msg['From'] = sender 36 | msg['X-Mailer'] = srv.SCRIPTNAME 37 | 38 | if not smtp_addresses: 39 | srv.logging.warning("Skipped sending SMTP notification to %s, " 40 | "no addresses configured" % (item.target)) 41 | return False 42 | 43 | try: 44 | srv.logging.debug("Sending SMTP notification to %s, addresses: %s" % (item.target, smtp_addresses)) 45 | server = smtplib.SMTP(server) 46 | server.set_debuglevel(0) 47 | server.ehlo() 48 | if starttls: 49 | server.starttls() 50 | if username: 51 | server.login(str(username), str(password)) 52 | server.sendmail(sender, smtp_addresses, msg.as_string()) 53 | server.quit() 54 | srv.logging.debug("Successfully sent SMTP notification") 55 | except Exception as e: 56 | srv.logging.warning("Error sending notification to SMTP recipient %s, addresses: %s. " 57 | "Exception: %s" % (item.target, smtp_addresses, e)) 58 | return False 59 | 60 | return True 61 | -------------------------------------------------------------------------------- /mqttwarn/services/sqlite.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Jan-Piet Mens ' 5 | __copyright__ = 'Copyright 2014 Jan-Piet Mens' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | import sqlite3 9 | 10 | 11 | def plugin(srv, item): 12 | """ sqlite. Expects addrs to contain (path, tablename) """ 13 | 14 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 15 | 16 | path = item.addrs[0] 17 | table = item.addrs[1] 18 | try: 19 | conn = sqlite3.connect(path) 20 | except Exception as e: 21 | srv.logging.warn("Cannot connect to sqlite at %s : %s" % (path, e)) 22 | return False 23 | 24 | c = conn.cursor() 25 | try: 26 | c.execute('CREATE TABLE IF NOT EXISTS %s (payload TEXT)' % table) 27 | except Exception as e: 28 | srv.logging.warn("Cannot create sqlite table in %s : %s" % (path, e)) 29 | return False 30 | 31 | text = item.message 32 | 33 | try: 34 | c.execute('INSERT INTO %s VALUES (?)' % table, (text, )) 35 | conn.commit() 36 | c.close() 37 | except Exception as e: 38 | srv.logging.warn("Cannot INSERT INTO sqlite:%s : %s" % (table, e)) 39 | 40 | return True 41 | -------------------------------------------------------------------------------- /mqttwarn/services/sqlite_timestamp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __originalauthor__ = 'Jan-Piet Mens ' 5 | __author__ = 'Kuthullu Himself ' 6 | __copyright__ = 'Copyright 2016 Kuthullu Himself' 7 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 8 | 9 | import sqlite3 10 | 11 | 12 | def plugin(srv, item): 13 | ''' sqlite. Expects addrs to contain (path, tablename) ''' 14 | 15 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 16 | 17 | path = item.addrs[0] 18 | table = item.addrs[1] 19 | try: 20 | conn = sqlite3.connect(path) 21 | except Exception as e: 22 | srv.logging.warn("Cannot connect to sqlite at %s : %s" % (path, e)) 23 | return False 24 | 25 | c = conn.cursor() 26 | try: 27 | c.execute('CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY AUTOINCREMENT, payload TEXT, timestamp DATETIME NOT NULL)' % table) 28 | except Exception as e: 29 | srv.logging.warn("Cannot create sqlite table in %s : %s" % (path, e)) 30 | return False 31 | 32 | text = item.message 33 | 34 | try: 35 | c.execute('INSERT INTO %s VALUES (NULL, ?, datetime(\'now\'))' % table, (text, )) 36 | conn.commit() 37 | c.close() 38 | except Exception as e: 39 | srv.logging.warn("Cannot INSERT INTO sqlite:%s : %s" % (table, e)) 40 | 41 | return True 42 | -------------------------------------------------------------------------------- /mqttwarn/services/ssh.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from six import string_types 3 | 4 | __author__ = 'David Ventura' 5 | __copyright__ = 'Copyright 2016 David Ventura' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | import os 9 | import json 10 | import paramiko 11 | from pipes import quote 12 | 13 | 14 | def credentials(host, user=None, pwd=None, port=22): 15 | c = {} 16 | if user is None and pwd is None: 17 | config = paramiko.SSHConfig() 18 | p = os.path.expanduser('~') + '/.ssh/config' 19 | config.parse(open(p)) 20 | o = config.lookup(host) 21 | 22 | ident = o['identityfile'] 23 | if type(ident) is list: 24 | ident = ident[0] 25 | if 'port' not in o: 26 | o['port'] = port 27 | c = {'hostname': o['hostname'], 'port': int(o['port']), 'username': o['user'], 'key_filename': ident} 28 | else: 29 | c = {'hostname': host, 'port': port, 'username': user, 'password': pwd} 30 | 31 | return c 32 | 33 | 34 | def plugin(srv, item): 35 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 36 | host = item.config["host"] 37 | port = 22 38 | user = None 39 | pwd = None 40 | 41 | if 'port' in item.config: 42 | port = item.config["port"] 43 | if 'user' in item.config: 44 | user = item.config["user"] 45 | if 'pass' in item.config: 46 | pwd = item.config["pass"] 47 | 48 | command = item.addrs[0] 49 | 50 | args = json.loads(item.payload)["args"] 51 | if type(args) is list and len(args) == 1: 52 | args = args[0] 53 | 54 | if isinstance(args, list): 55 | args = tuple([quote(v) for v in args]) # escape the shell args 56 | elif isinstance(args, string_types): 57 | args = (quote(args),) 58 | 59 | command = command % args 60 | 61 | ssh = paramiko.SSHClient() 62 | ssh.load_system_host_keys() 63 | ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 64 | 65 | try: 66 | c = credentials(host, user=user, pwd=pwd, port=port) 67 | ssh.connect(**c) 68 | _, stdout, stderr = ssh.exec_command(command) 69 | 70 | except Exception as e: 71 | srv.logging.warning("Cannot run command '%s' on host '%s'" % (command, host)) 72 | srv.logging.warning("%s" % e) 73 | return False 74 | 75 | return True 76 | -------------------------------------------------------------------------------- /mqttwarn/services/syslog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Fabian Affolter ' 5 | __copyright__ = 'Copyright 2014 Fabian Affolter' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | import syslog 9 | 10 | 11 | def plugin(srv, item): 12 | """Transfer a message to a syslog server.""" 13 | 14 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", 15 | __file__, item.service, item.target) 16 | 17 | facilities = { 18 | 'kernel' : syslog.LOG_KERN, 19 | 'user' : syslog.LOG_USER, 20 | 'mail' : syslog.LOG_MAIL, 21 | 'daemon' : syslog.LOG_DAEMON, 22 | 'auth' : syslog.LOG_KERN, 23 | 'LPR' : syslog.LOG_LPR, 24 | 'news' : syslog.LOG_NEWS, 25 | 'uucp' : syslog.LOG_UUCP, 26 | 'cron' : syslog.LOG_CRON, 27 | 'syslog' : syslog.LOG_SYSLOG, 28 | 'local0' : syslog.LOG_LOCAL0, 29 | 'local1' : syslog.LOG_LOCAL1, 30 | 'local2' : syslog.LOG_LOCAL2, 31 | 'local3' : syslog.LOG_LOCAL3, 32 | 'local4' : syslog.LOG_LOCAL4, 33 | 'local5' : syslog.LOG_LOCAL5, 34 | 'local6' : syslog.LOG_LOCAL6, 35 | 'local7' : syslog.LOG_LOCAL7 36 | } 37 | 38 | options = { 39 | 'pid' : syslog.LOG_PID, 40 | 'cons' : syslog.LOG_CONS, 41 | 'ndelay' : syslog.LOG_NDELAY, 42 | 'nowait' : syslog.LOG_NOWAIT, 43 | 'perror' : syslog.LOG_PERROR 44 | } 45 | 46 | priorities = { 47 | 5 : syslog.LOG_EMERG, 48 | 4 : syslog.LOG_ALERT, 49 | 3 : syslog.LOG_CRIT, 50 | 2 : syslog.LOG_ERR, 51 | 1 : syslog.LOG_WARNING, 52 | 0 : syslog.LOG_NOTICE, 53 | -1 : syslog.LOG_INFO, 54 | -2 : syslog.LOG_DEBUG 55 | } 56 | 57 | title = item.get('title', srv.SCRIPTNAME) 58 | facility = facilities[item.addrs[0]] 59 | option = options[item.addrs[1]] 60 | 61 | priority = priorities[item.get('priority', -1)] 62 | message = item.message 63 | 64 | try: 65 | srv.logging.debug("Message is going to syslog facility %s..." \ 66 | % ((item.target).upper())) 67 | syslog.openlog(title, option, facility) 68 | syslog.syslog(priority, message) 69 | srv.logging.debug("Successfully sent") 70 | syslog.closelog() 71 | except Exception as e: 72 | srv.logging.error("Error sending to syslog: %s" % e) 73 | syslog.closelog() 74 | return False 75 | 76 | return True 77 | -------------------------------------------------------------------------------- /mqttwarn/services/tootpaste.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function 4 | from builtins import str 5 | 6 | __author__ = 'Jan-Piet Mens ' 7 | __copyright__ = 'Copyright 2017 Jan-Piet Mens' 8 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 9 | 10 | import os 11 | import sys 12 | from mastodon import Mastodon 13 | 14 | 15 | def plugin(srv, item): 16 | _DEFAULT_URL = 'https://mastodon.social' 17 | 18 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 19 | 20 | # item.config is brought in from the configuration file 21 | config = item.config 22 | 23 | clientcreds, usercreds, base_url = item.addrs 24 | 25 | text = item.message 26 | 27 | try: 28 | mastodon = Mastodon( 29 | client_id=clientcreds, 30 | access_token=usercreds, 31 | api_base_url=base_url 32 | ) 33 | 34 | mastodon.toot(text) 35 | except Exception as e: 36 | srv.logging.warning("Cannot post to Mastodon: %s" % (str(e))) 37 | return False 38 | 39 | return True 40 | 41 | 42 | if __name__ == '__main__': 43 | from mastodon import Mastodon 44 | 45 | try: 46 | base_url, email, password, clientname, clientcred, usercred = sys.argv[1:] 47 | except: 48 | print("Usage: %s base_url email password clientname clientcredsfile usercredsfile" % sys.argv[0]) 49 | sys.exit(2) 50 | 51 | if not os.path.isfile(clientcred): 52 | Mastodon.create_app( 53 | clientname, 54 | to_file=clientcred, 55 | scopes=['read', 'write'], 56 | api_base_url=base_url) 57 | 58 | if not os.path.isfile(usercred): 59 | mastodon_api = Mastodon( 60 | client_id=clientcred, 61 | api_base_url=base_url) 62 | 63 | mastodon_api.log_in( 64 | email, 65 | password, 66 | to_file=usercred, 67 | scopes=['read', 'write'], 68 | ) 69 | -------------------------------------------------------------------------------- /mqttwarn/services/twilio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Jan-Piet Mens ' 5 | __copyright__ = 'Copyright 2014 Jan-Piet Mens' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | from twilio.rest import TwilioRestClient 9 | 10 | 11 | def plugin(srv, item): 12 | """ expects (accountSID, authToken, from, to) in addrs""" 13 | 14 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 15 | 16 | try: 17 | account_sid, auth_token, from_nr, to_nr = item.addrs 18 | except: 19 | srv.logging.warn("Twilio target is incorrectly configured") 20 | return False 21 | 22 | text = item.message 23 | 24 | try: 25 | client = TwilioRestClient(account_sid, auth_token) 26 | message = client.messages.create( 27 | body=text, 28 | to=to_nr, 29 | from_=from_nr) 30 | srv.logging.debug("Twilio returns %s" % (message.sid)) 31 | except Exception as e: 32 | srv.logging.warn("Twilio failed: %s" % e) 33 | return False 34 | 35 | return True 36 | -------------------------------------------------------------------------------- /mqttwarn/services/twitter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Jan-Piet Mens ' 5 | __copyright__ = 'Copyright 2014 Jan-Piet Mens' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | import twitter 9 | 10 | 11 | def plugin(srv, item): 12 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 13 | 14 | twitter_keys = item.addrs 15 | 16 | twapi = twitter.Api( 17 | consumer_key=twitter_keys[0], 18 | consumer_secret=twitter_keys[1], 19 | access_token_key=twitter_keys[2], 20 | access_token_secret=twitter_keys[3] 21 | ) 22 | 23 | text = item.message[0:138] 24 | try: 25 | srv.logging.debug("Sending tweet to %s..." % (item.target)) 26 | res = twapi.PostUpdate(text, trim_user=False) 27 | srv.logging.debug("Successfully sent tweet") 28 | except twitter.TwitterError as e: 29 | srv.logging.error("TwitterError: %s" % e) 30 | return False 31 | except Exception as e: 32 | srv.logging.error("Error sending tweet to %s: %s" % (item.target, e)) 33 | return False 34 | 35 | return True 36 | -------------------------------------------------------------------------------- /mqttwarn/services/websocket.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Giovanni Angoli ' 5 | __copyright__ = 'Copyright 2018 Giovanni Angoli' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | # This is basically the file.py service but designed for websockets, not more than s/file/websocket/g. 9 | 10 | import websocket 11 | 12 | 13 | def plugin(srv, item): 14 | 15 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 16 | 17 | 18 | # item.config is brought in from the configuration file 19 | config = item.config 20 | 21 | # addrs is a list[] associated with a particular target. 22 | # While it may contain more than one item (e.g. pushover) 23 | # the `websocket' service carries one only, i.e. a ws:// or wss:// uri 24 | uri = item.addrs[0].format(**item.data).encode('utf-8') 25 | 26 | # If the incoming payload has been transformed, use that, 27 | # else the original payload 28 | text = item.message 29 | 30 | try: 31 | ws = websocket.WebSocket() 32 | ws.connect(uri) 33 | ws.send(text) 34 | ws.close() 35 | except Exception as e: 36 | srv.logging.warning("Cannot write to websocket `%s': %s" % (uri, e)) 37 | return False 38 | 39 | return True 40 | -------------------------------------------------------------------------------- /mqttwarn/services/xbmc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Ben Jones ' 5 | __copyright__ = 'Copyright 2014 Ben Jones' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | from future import standard_library 9 | standard_library.install_aliases() 10 | 11 | import urllib.request, urllib.parse, urllib.error 12 | import base64 13 | try: 14 | import simplejson as json 15 | except ImportError: 16 | import json # type: ignore[no-redef] 17 | 18 | 19 | def plugin(srv, item): 20 | 21 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 22 | 23 | xbmchost = item.addrs[0] 24 | xbmcusername = None 25 | xbmcpassword = None 26 | 27 | if len(item.addrs) == 3: 28 | xbmcusername = item.addrs[1] 29 | xbmcpassword = item.addrs[2] 30 | 31 | title = item.title 32 | message = item.message 33 | image = item.image 34 | 35 | jsonparams = { 36 | "jsonrpc" : "2.0", 37 | "method" : "GUI.ShowNotification", 38 | "id" : 1, 39 | "params" : { 40 | "title" : title, 41 | "message" : message, 42 | "image" : image, 43 | "displaytime" : 10000 44 | } 45 | } 46 | jsoncommand = json.dumps(jsonparams).encode("utf-8") 47 | 48 | url = 'http://%s/jsonrpc' % (xbmchost) 49 | try: 50 | srv.logging.debug("Sending XBMC notification to %s [%s]..." % (item.target, xbmchost)) 51 | req = urllib.request.Request(url, jsoncommand) 52 | req.add_header("Content-type", "application/json") 53 | if xbmcpassword is not None: 54 | credentials = '%s:%s' % (xbmcusername, xbmcpassword) 55 | basicauth_token = base64.b64encode(credentials.encode('utf-8')).decode() 56 | authheader = "Basic %s" % basicauth_token 57 | req.add_header("Authorization", authheader) 58 | response = urllib.request.urlopen(req, timeout = 2) 59 | srv.logging.debug("Successfully sent XBMC notification") 60 | except urllib.error.URLError as e: 61 | srv.logging.error("URLError: %s" % e) 62 | return False 63 | except Exception as e: 64 | srv.logging.error("Error sending XBMC notification to %s [%s]: %s" % (item.target, xbmchost, e)) 65 | return False 66 | 67 | return True 68 | -------------------------------------------------------------------------------- /mqttwarn/services/xmpp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Fabian Affolter ' 5 | __copyright__ = 'Copyright 2014 Fabian Affolter' 6 | __license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)' 7 | 8 | import xmpp 9 | 10 | 11 | def plugin(srv, item): 12 | """Send a message to XMPP recipient(s).""" 13 | 14 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 15 | 16 | xmpp_addresses = item.addrs 17 | sender = item.config['sender'] 18 | password = item.config['password'] 19 | text = item.message 20 | 21 | if not xmpp_addresses: 22 | srv.logging.warn("Skipped sending XMPP notification to %s, " 23 | "no addresses configured" % (item.target)) 24 | return False 25 | 26 | try: 27 | srv.logging.debug("Sending XMPP notification to %s, addresses: %s" % (item.target, xmpp_addresses)) 28 | for target in xmpp_addresses: 29 | jid = xmpp.protocol.JID(sender) 30 | connection = xmpp.Client(jid.getDomain(),debug=[]) 31 | connection.connect() 32 | connection.auth(jid.getNode(), password, resource=jid.getResource()) 33 | connection.send(xmpp.protocol.Message(target, text)) 34 | srv.logging.debug("Successfully sent message") 35 | except Exception as e: 36 | srv.logging.error("Error sending message to %s: %s" % (item.target, e)) 37 | return False 38 | 39 | return True 40 | 41 | 42 | def xmpppy_monkeypatch_ssl(): 43 | """ 44 | Mitigate "AttributeError: '_ssl._SSLSocket' object has no attribute 'issuer'" 45 | 46 | Monkey-patched _startSSL method from 47 | https://raw.githubusercontent.com/freebsd/freebsd-ports/master/net-im/py-xmpppy/files/patch-xmpp-transports.py 48 | """ 49 | import ssl 50 | 51 | def _startSSL(self): 52 | """ Immidiatedly switch socket to TLS mode. Used internally.""" 53 | """ Here we should switch pending_data to hint mode.""" 54 | tcpsock=self._owner.Connection 55 | tcpsock._sslObj = ssl.wrap_socket(tcpsock._sock, None, None) 56 | tcpsock._sslIssuer = tcpsock._sslObj.getpeercert().get('issuer') 57 | tcpsock._sslServer = tcpsock._sslObj.getpeercert().get('server') 58 | tcpsock._recv = tcpsock._sslObj.read 59 | tcpsock._send = tcpsock._sslObj.write 60 | 61 | tcpsock._seen_data=1 62 | self._tcpsock=tcpsock 63 | tcpsock.pending_data=self.pending_data 64 | tcpsock._sock.setblocking(0) 65 | 66 | self.starttls='success' 67 | 68 | from xmpp.transports import TLS 69 | TLS._startSSL = _startSSL 70 | -------------------------------------------------------------------------------- /mqttwarn/testing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/mqttwarn/testing/__init__.py -------------------------------------------------------------------------------- /mqttwarn/testing/fixtures.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pytest 4 | 5 | from mqttwarn.model import Service 6 | 7 | 8 | @pytest.fixture 9 | def mqttwarn_service(): 10 | """ 11 | A service instance for propagating to the plugin. 12 | """ 13 | logger = logging.getLogger(__name__) 14 | # FIXME: Should propagate `mqttwarn.core.globals()` to `mwcore`. 15 | return Service(mqttc=None, logger=logger, mwcore={}, program="mqttwarn-testdrive") 16 | -------------------------------------------------------------------------------- /mqttwarn/vendor/ZabbixSender.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function 4 | from builtins import str 5 | import socket 6 | import struct 7 | 8 | # Single file, imported from https://github.com/BlueSkyDetector/code-snippet/tree/master/ZabbixSender 9 | # Lincense: DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 10 | # Version 2, December 2004 11 | # Copyright (C) 2010 Takanori Suzuki 12 | 13 | # JPM: s/simplejson/json/g 14 | 15 | try: 16 | import json 17 | except ImportError: 18 | import simplejson as json # type: ignore[no-redef] 19 | 20 | 21 | class ZabbixSender: 22 | 23 | zbx_header = b'ZBXD' 24 | zbx_version = 1 25 | zbx_sender_data = {'request': 'sender data', 'data': []} 26 | send_data = '' 27 | 28 | def __init__(self, server_host, server_port = 10051): 29 | self.server_ip = socket.gethostbyname(server_host) 30 | self.server_port = server_port 31 | 32 | def AddData(self, host, key, value, clock = None): 33 | add_data = {'host': host, 'key': key, 'value': value} 34 | if clock != None: 35 | add_data['clock'] = clock 36 | self.zbx_sender_data['data'].append(add_data) 37 | return self.zbx_sender_data 38 | 39 | def ClearData(self): 40 | self.zbx_sender_data['data'] = [] 41 | return self.zbx_sender_data 42 | 43 | def __MakeSendData(self): 44 | zbx_sender_json = json.dumps(self.zbx_sender_data, separators=(',', ':'), ensure_ascii=False).encode('utf-8') 45 | json_byte = len(zbx_sender_json) 46 | self.send_data = struct.pack("<4sBq" + str(json_byte) + "s", self.zbx_header, self.zbx_version, json_byte, zbx_sender_json) 47 | 48 | def Send(self): 49 | self.__MakeSendData() 50 | so = socket.socket() 51 | so.connect((self.server_ip, self.server_port)) 52 | wobj = so.makefile('wb') 53 | wobj.write(self.send_data) 54 | wobj.close() 55 | robj = so.makefile('rb') 56 | recv_data = robj.read() 57 | robj.close() 58 | so.close() 59 | tmp_data = struct.unpack("<4sBq" + str(len(recv_data) - struct.calcsize("<4sBq")) + "s", recv_data) 60 | recv_json = json.loads(tmp_data[3]) 61 | #JPM return recv_data 62 | return recv_json 63 | 64 | 65 | if __name__ == '__main__': 66 | sender = ZabbixSender('127.0.0.1') 67 | for num in range(0, 2): 68 | sender.AddData('HostA', 'AppX_Logger', 'sent data 第' + str(num)) 69 | res = sender.Send() 70 | print(sender.send_data) 71 | print(res) 72 | -------------------------------------------------------------------------------- /mqttwarn/vendor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/mqttwarn/vendor/__init__.py -------------------------------------------------------------------------------- /requirements-release.txt: -------------------------------------------------------------------------------- 1 | keyring 2 | twine 3 | -------------------------------------------------------------------------------- /templates/demo.j2: -------------------------------------------------------------------------------- 1 | {# 2 | this is a comment 3 | in Jinja2 4 | See http://jinja.pocoo.org/docs/templates/ for information 5 | on Jinja2 templates. 6 | #} 7 | {% set upname = name | upper %} 8 | {% set width = 60 %} 9 | {% for n in range(0, width) %}-{% endfor %} 10 | 11 | Name.................: {{ upname }} 12 | Number...............: {{ number }} 13 | Timestamp............: {{ _dthhmm }} 14 | Original payload.....: {{ payload }} 15 | -------------------------------------------------------------------------------- /templates/hiveeyes-alert.j2: -------------------------------------------------------------------------------- 1 | {# 2 | Hiveeyes alert template 3 | #} 4 | Alarm from beehive "{{node}}". 5 | {{description}} 6 | 7 | {% print '-' * 42 %} 8 | 9 | Network..............: {{ network }} 10 | Gateway..............: {{ gateway }} 11 | Node.................: {{ node }} 12 | Timestamp UTC........: {{ _dtiso }} 13 | Current data.........: {{ pformat(history['current'], width=120) }} 14 | Previous data........: {{ pformat(history['previous'], width=120) }} 15 | {% print '-' * 42 %} 16 | 17 | Current fragment.....: {{ pformat(fragments['current'], width=120) }} 18 | Previous fragment....: {{ pformat(fragments['previous'], width=120) }} 19 | Original payload.....: {{ payload }} 20 | {% print '-' * 42 %} 21 | 22 | Grafana dashboard....: https://swarm.hiveeyes.org/grafana/dashboard/db/{{ dashboard }} 23 | -------------------------------------------------------------------------------- /templates/test.jinja: -------------------------------------------------------------------------------- 1 | {# This is a comment in Jinja2. #} 2 | {% set upname = name | upper %} 3 | Name: {{ upname }} 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2018 The mqttwarn developers 3 | 4 | # Configuration- and function files used by the test harness 5 | configfile_full = "tests/etc/full.ini" 6 | configfile_service_loading = "tests/etc/service-loading.ini" 7 | configfile_no_functions = "tests/etc/no-functions.ini" 8 | configfile_empty_functions = "tests/etc/empty-functions.ini" 9 | configfile_logging_levels = "tests/etc/logging-levels.ini" 10 | configfile_better_addresses = "tests/etc/better-addresses.ini" 11 | configfile_with_variables = "tests/etc/with-variables.ini" 12 | funcfile_good = "tests/etc/functions_good.py" 13 | funcfile_bad = "tests/etc/functions_bad.py" 14 | -------------------------------------------------------------------------------- /tests/acme/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/tests/acme/__init__.py -------------------------------------------------------------------------------- /tests/acme/foobar.py: -------------------------------------------------------------------------------- 1 | def plugin(srv, item): 2 | srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target) 3 | srv.logging.info("Plugin invoked") 4 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2018-2023 The mqttwarn developers 3 | import importlib 4 | import os 5 | import pathlib 6 | import shutil 7 | import sys 8 | import typing as t 9 | from tempfile import NamedTemporaryFile 10 | 11 | import pytest 12 | 13 | # Needed to make Apprise not be mocked too much. 14 | from mqttwarn.services.apprise_util import get_all_template_argument_names # noqa:F401 15 | 16 | # Import custom fixtures. 17 | from mqttwarn.testing.fixtures import mqttwarn_service as srv # noqa:F401 18 | from tests.fixtures.ntfy import ntfy_service # noqa:F401 19 | 20 | 21 | @pytest.fixture 22 | def fake_filesystem(fs): # pylint:disable=invalid-name 23 | try: 24 | fs.create_dir("/tmp") 25 | except Exception: 26 | pass 27 | yield fs 28 | 29 | 30 | @pytest.fixture 31 | def without_jinja(): 32 | 33 | # Emulate removal of `jinja2` package. 34 | # https://stackoverflow.com/a/65163627 35 | backup = sys.modules["jinja2"] 36 | sys.modules["jinja2"] = None 37 | importlib.reload(sys.modules["mqttwarn.core"]) 38 | 39 | yield 40 | 41 | # Restore `jinja2` package. 42 | sys.modules["jinja2"] = backup 43 | importlib.reload(sys.modules["mqttwarn.core"]) 44 | 45 | 46 | @pytest.fixture 47 | def without_ssl(): 48 | 49 | # Emulate removal of `ssl` package. 50 | # https://stackoverflow.com/a/65163627 51 | backup = sys.modules["ssl"] 52 | sys.modules["ssl"] = None 53 | importlib.reload(sys.modules["mqttwarn.configuration"]) 54 | 55 | yield 56 | 57 | # Restore `jinja2` package. 58 | sys.modules["ssl"] = backup 59 | importlib.reload(sys.modules["mqttwarn.configuration"]) 60 | 61 | 62 | @pytest.fixture 63 | def mqttwarn_bin(): 64 | """ 65 | Find `mqttwarn` executable, located within the inline virtualenv. 66 | """ 67 | 68 | path_candidates = [None, ".venv/bin", r".venv\Scripts"] 69 | for path_candidate in path_candidates: 70 | mqttwarn_bin = shutil.which("mqttwarn", path=path_candidate) 71 | if mqttwarn_bin is not None: 72 | return mqttwarn_bin 73 | 74 | raise FileNotFoundError(f"Unable to discover 'mqttwarn' executable within {path_candidates}") 75 | 76 | 77 | @pytest.fixture() 78 | def tmp_ini(tmp_path) -> pathlib.Path: 79 | """ 80 | Provide temporary INI files to test cases. 81 | """ 82 | filepath = tmp_path.joinpath("testdrive.ini") 83 | return filepath 84 | 85 | 86 | @pytest.fixture 87 | def attachment_dummy() -> t.Generator[t.IO[bytes], None, None]: 88 | """ 89 | Provide a temporary file to the test cases to be used as an attachment with defined content. 90 | """ 91 | tmp = NamedTemporaryFile(suffix=".txt", delete=False) 92 | tmp.write(b"foo") 93 | tmp.close() 94 | yield tmp 95 | os.unlink(tmp.name) 96 | -------------------------------------------------------------------------------- /tests/etc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/tests/etc/__init__.py -------------------------------------------------------------------------------- /tests/etc/better-addresses.ini: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2023 The mqttwarn developers 3 | # 4 | # mqttwarn configuration file for testing "improved 5 | # addresses configuration", with named parameters. 6 | # 7 | 8 | ; ------- 9 | ; General 10 | ; ------- 11 | 12 | [defaults] 13 | 14 | 15 | ; ---- 16 | ; MQTT 17 | ; ---- 18 | 19 | hostname = 'localhost' 20 | port = 1883 21 | username = None 22 | password = None 23 | clientid = 'mqttwarn-testdrive' 24 | lwt = 'clients/mqttwarn-testdrive' 25 | skipretained = False 26 | cleansession = False 27 | 28 | # MQTTv31 = 3 (default) 29 | # MQTTv311 = 4 30 | protocol = 3 31 | 32 | 33 | ; ------- 34 | ; Logging 35 | ; ------- 36 | 37 | ; Send log output to STDERR 38 | logfile = 'stream://sys.stderr' 39 | 40 | ; Send log output to file 41 | ;logfile = 'mqttwarn.log' 42 | 43 | ; one of: CRITICAL, DEBUG, ERROR, INFO, WARN 44 | loglevel = DEBUG 45 | 46 | 47 | ; -------- 48 | ; Services 49 | ; -------- 50 | 51 | ; name the service providers you will be using. 52 | launch = apprise 53 | 54 | [config:apprise] 55 | ; Dispatch message to multiple Apprise plugins. 56 | module = 'apprise_multi' 57 | targets = { 58 | 'demo-http' : [ { 'baseuri': 'json://localhost:1234/mqtthook' }, { 'baseuri': 'json://daq.example.org:5555/foobar' } ], 59 | 'demo-discord' : [ { 'baseuri': 'discord://4174216298/JHMHI8qBe7bk2ZwO5U711o3dV_js' } ], 60 | 'demo-mailto' : [ { 61 | 'baseuri': 'mailtos://smtp_username:smtp_password@mail.example.org', 62 | 'recipients': ['foo@example.org', 'bar@example.org'], 63 | 'sender': 'monitoring@example.org', 64 | 'sender_name': 'Example Monitoring', 65 | } ], 66 | } 67 | 68 | 69 | [config:pushsafer] 70 | ; https://www.pushsafer.com/en/pushapi 71 | ; https://www.pushsafer.com/en/pushapi_ext 72 | targets = { 73 | 'basic': { 'private_key': '3SAz1a2iTYsh19eXIMiO' }, 74 | 'nagios': { 75 | 'private_key': '3SAz1a2iTYsh19eXIMiO', 76 | 'device': '52|65|78', 77 | 'icon': 64, 78 | 'sound': 2, 79 | 'vibration': 1, 80 | 'url': 'http://example.org', 81 | 'url_title': 'Example Org', 82 | 'time_to_live': 60, 83 | 'priority': 2, 84 | 'retry': 60, 85 | 'expire': 600, 86 | 'answer': 1, 87 | }, 88 | 'tracking': { 89 | 'private_key': '3SAz1a2iTYsh19eXIMiO', 90 | 'device': 'gs23', 91 | 'icon': 18, 92 | }, 93 | 'bogus': { 'foo': 'bar' }, 94 | } 95 | 96 | 97 | 98 | ; ------- 99 | ; Targets 100 | ; ------- 101 | 102 | [apprise-test] 103 | topic = apprise/# 104 | targets = apprise:demo-http, apprise:demo-discord, apprise:demo-mailto 105 | format = Alarm from {device}: {payload} 106 | title = Alarm from {device} 107 | -------------------------------------------------------------------------------- /tests/etc/empty-functions.ini: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2014-2022 The mqttwarn developers 3 | # 4 | # mqttwarn configuration file for testing with empty user defined `functions` setting. 5 | # 6 | 7 | ; ------- 8 | ; General 9 | ; ------- 10 | 11 | [defaults] 12 | 13 | 14 | ; This is an *empty* `functions` setting. 15 | functions = 16 | 17 | ; name the service providers you will be using. 18 | launch = log 19 | 20 | 21 | ; -------- 22 | ; Services 23 | ; -------- 24 | 25 | [config:log] 26 | targets = { 27 | 'debug' : [ 'debug' ], 28 | 'info' : [ 'info' ], 29 | 'warn' : [ 'warn' ], 30 | 'crit' : [ 'crit' ], 31 | 'error' : [ 'error' ] 32 | } 33 | 34 | 35 | ; ------- 36 | ; Targets 37 | ; ------- 38 | 39 | [test/log-1] 40 | ; echo '{"name": "temperature", "value": 42.42}' | mosquitto_pub -h localhost -t test/log-1 -l 41 | targets = log:info 42 | format = {name}: {value} 43 | -------------------------------------------------------------------------------- /tests/etc/functions_bad.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2020 The mqttwarn developers 3 | 4 | def foobar(): 5 | """ 6 | This function has an intentional indentation error, for 7 | validating mqttwarn runtime behavior with bogus Python code. 8 | """ 9 | foo 10 | return True 11 | -------------------------------------------------------------------------------- /tests/etc/functions_good.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2018-2022 The mqttwarn developers 3 | import logging 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | def foobar(): 9 | return True 10 | 11 | 12 | def cronfunc(srv): 13 | logger.info("`cronfunc` called") 14 | return True 15 | 16 | 17 | def xform_func(data): 18 | data["xform-key"] = "xform-value" 19 | return data 20 | 21 | 22 | def datamap_dummy_v1(topic): 23 | return {"datamap-key": "datamap-value"} 24 | 25 | 26 | def datamap_dummy_v2(topic, srv): 27 | return {"datamap-key": "datamap-value"} 28 | 29 | 30 | def alldata_dummy(topic, data, srv): 31 | return {"alldata-key": "alldata-value"} 32 | 33 | 34 | def filter_dummy_v1(topic, message): 35 | do_skip = "reject" in message 36 | return do_skip 37 | 38 | 39 | def filter_dummy_v2(topic, message, section, srv): 40 | do_skip = "reject" in message 41 | return do_skip 42 | 43 | 44 | def get_targets_valid(srv, topic, data): 45 | return ["log:info"] 46 | 47 | 48 | def get_targets_invalid(srv, topic, data): 49 | return ["log:invalid"] 50 | 51 | 52 | def get_targets_broken(srv, topic, data): 53 | return "broken" 54 | 55 | 56 | def get_targets_error(srv, topic, data): 57 | raise ValueError("Something failed") 58 | -------------------------------------------------------------------------------- /tests/etc/logging-levels.ini: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2014-2022 The mqttwarn developers 3 | # 4 | # mqttwarn configuration file for testing log level tuning 5 | # 6 | 7 | ; ------- 8 | ; General 9 | ; ------- 10 | 11 | [defaults] 12 | 13 | ; ------- 14 | ; Logging 15 | ; ------- 16 | 17 | filteredmessagesloglevel = DEBUG 18 | 19 | ; -------- 20 | ; Services 21 | ; -------- 22 | 23 | ; path to file containing self-defined functions like `format` or `alldata` 24 | ; generate with "mqttwarn make-udf" 25 | functions = 'tests/etc/functions_good.py' 26 | 27 | ; name the service providers you will be using. 28 | launch = log, file, tests.acme.foobar, tests/acme/foobar.py 29 | 30 | [config:log] 31 | targets = { 32 | 'debug' : [ 'debug' ], 33 | 'info' : [ 'info' ], 34 | 'warn' : [ 'warn' ], 35 | 'crit' : [ 'crit' ], 36 | 'error' : [ 'error' ], 37 | 'invalid': [ 'invalid' ], 38 | 'broken': 'broken' 39 | } 40 | 41 | [config:file] 42 | append_newline = True 43 | targets = { 44 | 'test-1' : ['$TMPDIR/mqttwarn-test.01'], 45 | 'test-2' : ['$TMPDIR/mqttwarn-test.02'], 46 | } 47 | 48 | [config:tests.acme.foobar] 49 | targets = { 50 | 'default' : [ 'default' ], 51 | } 52 | 53 | [config:tests/acme/foobar.py] 54 | targets = { 55 | 'default' : [ 'default' ], 56 | } 57 | 58 | ; ------- 59 | ; Targets 60 | ; ------- 61 | 62 | [test/filter-1] 63 | targets = log:info 64 | filter = filter_dummy_v1() 65 | -------------------------------------------------------------------------------- /tests/etc/no-functions.ini: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2014-2021 The mqttwarn developers 3 | # 4 | # mqttwarn configuration file for testing without user defined `functions` setting. 5 | # 6 | 7 | ; ------- 8 | ; General 9 | ; ------- 10 | 11 | [defaults] 12 | 13 | ; This is *without* a `functions` setting. 14 | ;functions = 'DO NOT SET' 15 | 16 | ; name the service providers you will be using. 17 | launch = log, file 18 | 19 | 20 | ; -------- 21 | ; Services 22 | ; -------- 23 | 24 | [config:log] 25 | targets = { 26 | 'debug' : [ 'debug' ], 27 | 'info' : [ 'info' ], 28 | 'warn' : [ 'warn' ], 29 | 'crit' : [ 'crit' ], 30 | 'error' : [ 'error' ] 31 | } 32 | 33 | [config:file] 34 | targets = { 35 | 'spool-binary': ['/tmp/mqttwarn-test-spool.jpg'], 36 | } 37 | 38 | # Pass-through payload content 1:1. 39 | append_newline = False 40 | decode_utf8 = False 41 | overwrite = True 42 | 43 | 44 | ; ------- 45 | ; Targets 46 | ; ------- 47 | 48 | [test/log-1] 49 | ; echo '{"name": "temperature", "value": 42.42}' | mosquitto_pub -h localhost -t test/log-1 -l 50 | targets = log:info 51 | format = {name}: {value} 52 | 53 | [test/file-1] 54 | ; wget -O goat.png https://user-images.githubusercontent.com/453543/231550862-5a64ac7c-bdfa-4509-86b8-b1a770899647.png 55 | ; mosquitto_pub -f goat.png -t 'test/file-1' 56 | targets = file:spool-binary 57 | -------------------------------------------------------------------------------- /tests/etc/password.txt: -------------------------------------------------------------------------------- 1 | secret-password -------------------------------------------------------------------------------- /tests/etc/service-loading.ini: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2014-2021 The mqttwarn developers 3 | # 4 | # mqttwarn configuration file for testing function and module loading with relative paths. 5 | # 6 | 7 | ; ------- 8 | ; General 9 | ; ------- 10 | 11 | [defaults] 12 | 13 | 14 | ; path to file containing self-defined functions like `format` or `alldata` 15 | ; generate with "mqttwarn make-udf" 16 | functions = 'functions_good.py' 17 | 18 | ; name the service providers you will be using. 19 | launch = tests.acme.foobar, ../acme/foobar.py 20 | 21 | 22 | ; -------- 23 | ; Services 24 | ; -------- 25 | 26 | [config:tests.acme.foobar] 27 | targets = { 28 | 'default' : [ 'default' ], 29 | } 30 | 31 | [config:../acme/foobar.py] 32 | targets = { 33 | 'default' : [ 'default' ], 34 | } 35 | 36 | 37 | ; ------- 38 | ; Targets 39 | ; ------- 40 | 41 | [test/plugin-module] 42 | ; echo '{"name": "temperature", "value": 42.42}' | mosquitto_pub -h localhost -t test/plugin-module -l 43 | targets = tests.acme.foobar:default 44 | format = {name}: {value} 45 | 46 | [test/plugin-file] 47 | ; echo '{"name": "temperature", "value": 42.42}' | mosquitto_pub -h localhost -t test/plugin-file -l 48 | targets = ../acme/foobar.py:default 49 | format = {name}: {value} 50 | -------------------------------------------------------------------------------- /tests/etc/with-variables.ini: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2023 The mqttwarn developers 3 | # 4 | # mqttwarn configuration file for testing variable expansion. 5 | # 6 | 7 | ; ------- 8 | ; General 9 | ; ------- 10 | 11 | [defaults] 12 | hostname = $ENV:HOSTNAME 13 | port = $ENV:PORT 14 | username = ${ENV:USERNAME} 15 | password = ${FILE:./password.txt} 16 | 17 | ; name the service providers you will be using. 18 | launch = file 19 | 20 | 21 | ; -------- 22 | ; Services 23 | ; -------- 24 | 25 | [config:file] 26 | targets = { 27 | 'mylog' : [ '$ENV:LOG_FILE' ], 28 | } 29 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/ntfy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2023 Andreas Motl 3 | # 4 | # Use of this source code is governed by an MIT-style 5 | # license that can be found in the LICENSE file or at 6 | # https://opensource.org/licenses/MIT. 7 | """ 8 | Provide the `ntfy`_ API service as a session-scoped fixture to your test 9 | harness. 10 | 11 | Source: https://docs.ntfy.sh/install/#docker 12 | 13 | .. _ntfy: https://ntfy.sh/ 14 | """ 15 | import docker 16 | import pytest 17 | from pytest_docker_fixtures import images 18 | from pytest_docker_fixtures.containers._base import BaseImage 19 | from pytest_mqtt.util import probe_tcp_connect 20 | 21 | images.settings["ntfy"] = { 22 | "image": "binwiederhier/ntfy", 23 | "version": "latest", 24 | "options": { 25 | "command": """ 26 | serve 27 | --base-url="http://localhost:5555" 28 | --attachment-cache-dir="/tmp/ntfy-attachments" 29 | """, 30 | "publish_all_ports": False, 31 | "ports": {"80/tcp": "5555"}, 32 | }, 33 | } 34 | 35 | 36 | class Ntfy(BaseImage): 37 | 38 | name = "ntfy" 39 | 40 | def check(self): 41 | # TODO: Add real implementation. 42 | return True 43 | 44 | def pull_image(self): 45 | """ 46 | Image needs to be pulled explicitly. 47 | Workaround against `404 Client Error: Not Found for url: http+docker://localhost/v1.23/containers/create`. 48 | 49 | - https://github.com/mqtt-tools/mqttwarn/pull/589#issuecomment-1249680740 50 | - https://github.com/docker/docker-py/issues/2101 51 | """ 52 | docker_client = docker.from_env(version=self.docker_version) 53 | image_name = self.image 54 | docker_client.images.pull(image_name) 55 | 56 | def run(self): 57 | self.pull_image() 58 | super(Ntfy, self).run() 59 | 60 | 61 | ntfy_image = Ntfy() 62 | 63 | 64 | def is_ntfy_running() -> bool: 65 | return probe_tcp_connect("localhost", 5555) 66 | 67 | 68 | @pytest.fixture(scope="session") 69 | def ntfy_service(): 70 | 71 | # Gracefully skip spinning up the Docker container if ntfy is already running. 72 | if is_ntfy_running(): 73 | yield "localhost", 5555 74 | return 75 | 76 | ntfy_image.run() 77 | yield "localhost", 5555 78 | ntfy_image.stop() 79 | -------------------------------------------------------------------------------- /tests/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/tests/services/__init__.py -------------------------------------------------------------------------------- /tests/services/pushsafer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqtt-tools/mqttwarn/d9d312df66b428bf36c053ebaa88e6de18f8aa37/tests/services/pushsafer/__init__.py -------------------------------------------------------------------------------- /tests/services/pushsafer/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.util import FakeResponse 4 | 5 | 6 | @pytest.fixture 7 | def mock_urlopen_success(mocker): 8 | return mocker.patch("urllib.request.urlopen", return_value=FakeResponse(data=b'{"status": 1}')) 9 | 10 | 11 | @pytest.fixture 12 | def mock_urlopen_failure(mocker): 13 | return mocker.patch("urllib.request.urlopen", return_value=FakeResponse(data=b'{"status": 6}')) 14 | -------------------------------------------------------------------------------- /tests/services/pushsafer/test_pushsafer_common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2023 The mqttwarn developers 3 | """ 4 | This file contains common cases for the Pushsafer plugin, independently 5 | of the used configuration layout variant (v1 vs. v2) within the `addrs` slot. 6 | """ 7 | import pytest 8 | 9 | from mqttwarn.model import ProcessorItem as Item 10 | from mqttwarn.services.pushsafer import PushsaferParameters 11 | from mqttwarn.util import load_module_from_file 12 | 13 | 14 | def test_pushsafer_configuration_empty_failure(srv, caplog, mock_urlopen_success): 15 | """ 16 | Test Pushsafer service fails when providing an empty `addrs` configuration slot. 17 | """ 18 | 19 | module = load_module_from_file("mqttwarn/services/pushsafer.py") 20 | item = Item(addrs=None, message="⚽ Notification message ⚽") 21 | with pytest.raises(ValueError) as ex: 22 | module.plugin(srv, item) 23 | assert ex.match("Pushsafer configuration layout empty or invalid. type=NoneType") 24 | 25 | 26 | def test_pushsafer_configuration_invalid_failure(srv, caplog, mock_urlopen_success): 27 | """ 28 | Test Pushsafer service fails when providing an invalid `addrs` configuration slot. 29 | """ 30 | 31 | module = load_module_from_file("mqttwarn/services/pushsafer.py") 32 | item = Item(addrs=42, message="⚽ Notification message ⚽") 33 | with pytest.raises(ValueError) as ex: 34 | module.plugin(srv, item) 35 | assert ex.match("Pushsafer configuration layout empty or invalid. type=int") 36 | 37 | 38 | def test_pushsafer_parameters_to_dict(): 39 | pp = PushsaferParameters(private_key="foo", device="bar") 40 | result = pp.to_dict() 41 | assert isinstance(result, dict) 42 | assert "private_key" in result 43 | assert "device" in result 44 | -------------------------------------------------------------------------------- /tests/services/pushsafer/util.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | import urllib.request 3 | from urllib.parse import parse_qsl 4 | 5 | TEST_TOKEN = "myToken" 6 | 7 | 8 | def get_reference_data(**more_data): 9 | data = { 10 | "m": "⚽ Notification message ⚽", 11 | "k": "myToken", 12 | } 13 | data.update(more_data) 14 | return data 15 | 16 | 17 | def assert_request(request: urllib.request.Request, reference_data: t.Dict[str, str]): 18 | assert request.full_url == "https://www.pushsafer.com/api" 19 | if isinstance(request.data, bytes): 20 | payload = request.data 21 | elif hasattr(request.data, "read"): 22 | payload = request.data.read() # type: ignore[union-attr] 23 | else: 24 | raise ValueError(f"Something went wrong. Could not decode `request.data`: {request.data}") 25 | actual_data = dict(parse_qsl(payload.decode("utf-8"), keep_blank_values=True)) 26 | msg = f"\nGot: {actual_data}\nExpected: {reference_data}" 27 | assert actual_data == reference_data, msg 28 | -------------------------------------------------------------------------------- /tests/services/test_alexa.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2021-2022 The mqttwarn developers 3 | from mqttwarn.model import ProcessorItem as Item 4 | from mqttwarn.util import load_module_from_file 5 | 6 | 7 | def test_alexa_notify_me_success(srv, mocker, caplog): 8 | 9 | module = load_module_from_file("mqttwarn/services/alexa-notify-me.py") 10 | 11 | accessCode = "myToken" 12 | item = Item(addrs=[accessCode], message="⚽ Notification message ⚽") 13 | 14 | requests_mock = mocker.patch("requests.post") 15 | outcome = module.plugin(srv, item) 16 | requests_mock.assert_called_once_with( 17 | url="https://api.notifymyecho.com/v1/NotifyMe", 18 | data='{"notification": "\\u26bd Notification message \\u26bd", "accessCode": "myToken"}', 19 | ) 20 | 21 | assert outcome is True 22 | assert "Sending to NotifyMe service" in caplog.messages 23 | assert "Successfully sent to NotifyMe service" in caplog.messages 24 | 25 | 26 | def test_alexa_notify_me_real_auth_failure(srv, caplog): 27 | module = load_module_from_file("mqttwarn/services/alexa-notify-me.py") 28 | 29 | accessCode = "myToken" 30 | item = Item(addrs=[accessCode], message="⚽ Notification message ⚽") 31 | 32 | outcome = module.plugin(srv, item) 33 | 34 | assert outcome is False 35 | assert "Sending to NotifyMe service" in caplog.messages 36 | assert "Failed to send message to NotifyMe service" in caplog.text 37 | -------------------------------------------------------------------------------- /tests/services/test_apprise_ntfy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2021-2023 The mqttwarn developers 3 | from unittest import mock 4 | from unittest.mock import call 5 | 6 | from mqttwarn.model import ProcessorItem as Item 7 | from mqttwarn.util import load_module_by_name 8 | 9 | 10 | @mock.patch("apprise.Apprise", create=True) 11 | @mock.patch("apprise.AppriseAsset", create=True) 12 | def test_apprise_ntfy_success(apprise_asset, apprise_mock, srv, caplog): 13 | module = load_module_by_name("mqttwarn.services.apprise_multi") 14 | 15 | item = Item( 16 | addrs=[ 17 | { 18 | "baseuri": "ntfy://user:password@ntfy.example.org/topic1/topic2?email=test@example.org", 19 | } 20 | ], 21 | title="⚽ Message title ⚽", 22 | message="⚽ Notification message ⚽", 23 | data={"priority": "high", "tags": "foo,bar", "click": "https://httpbin.org/headers"}, 24 | ) 25 | 26 | outcome = module.plugin(srv, item) 27 | 28 | assert apprise_mock.mock_calls == [ 29 | call(asset=mock.ANY), 30 | call().add( 31 | "ntfy://user:password@ntfy.example.org/topic1/topic2?email=test@example.org" 32 | "&click=https%3A%2F%2Fhttpbin.org%2Fheaders&priority=high&tags=foo%2Cbar" 33 | ), 34 | call().notify(body="⚽ Notification message ⚽", title="⚽ Message title ⚽"), 35 | call().notify().__bool__(), 36 | ] 37 | 38 | assert "Successfully sent message using Apprise" in caplog.messages 39 | assert outcome is True 40 | -------------------------------------------------------------------------------- /tests/services/test_autoremote.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2021-2022 The mqttwarn developers 3 | from mqttwarn.model import ProcessorItem as Item 4 | from mqttwarn.util import load_module_from_file 5 | 6 | 7 | def test_autoremote_success(srv, mocker, caplog): 8 | 9 | item = Item( 10 | target="test", 11 | addrs=["ApiKey", "Password", "Target", "Group", "TTL"], 12 | topic="autoremote/user", 13 | message="⚽ Notification message ⚽", 14 | ) 15 | 16 | module = load_module_from_file("mqttwarn/services/autoremote.py") 17 | 18 | requests_mock = mocker.patch("requests.get") 19 | 20 | outcome = module.plugin(srv, item) 21 | requests_mock.assert_called_once_with( 22 | "https://autoremotejoaomgcd.appspot.com/sendmessage", 23 | params={ 24 | "key": "ApiKey", 25 | "message": "⚽ Notification message ⚽", 26 | "target": "Target", 27 | "sender": "autoremote/user", 28 | "password": "Password", 29 | "ttl": "TTL", 30 | "collapseKey": "Group", 31 | }, 32 | ) 33 | 34 | assert outcome is True 35 | assert "Sending to autoremote service" in caplog.messages 36 | assert "Successfully sent to autoremote service" in caplog.messages 37 | 38 | 39 | def test_autoremote_failure(srv, mocker, caplog): 40 | 41 | item = Item( 42 | target="test", 43 | addrs=["ApiKey", "Password", "Target", "Group", "TTL"], 44 | topic="autoremote/user", 45 | message="⚽ Notification message ⚽", 46 | ) 47 | 48 | module = load_module_from_file("mqttwarn/services/autoremote.py") 49 | 50 | requests_mock = mocker.patch("requests.get", side_effect=Exception("something failed")) 51 | 52 | outcome = module.plugin(srv, item) 53 | requests_mock.assert_called_once() 54 | 55 | assert outcome is False 56 | assert "Sending to autoremote service" in caplog.messages 57 | assert "Failed to send message to autoremote service: something failed" in caplog.messages 58 | -------------------------------------------------------------------------------- /tests/services/test_execute.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2023 The mqttwarn developers 3 | import sys 4 | 5 | import pytest 6 | 7 | import mqttwarn.services.execute 8 | from mqttwarn.model import ProcessorItem as Item 9 | 10 | 11 | @pytest.mark.skipif(sys.platform == "win32", reason="This test does not work on Windows") 12 | def test_execute_success(tmp_path, srv, caplog, capfd): 13 | """ 14 | Dispatch a single command invocation, and verify it worked. 15 | """ 16 | 17 | tmpfile = tmp_path / "spool.txt" 18 | tmpfile.write_text("Hello, world.") 19 | 20 | module = mqttwarn.services.execute 21 | 22 | item = Item( 23 | target="test", 24 | addrs=["cat", "[TEXT]"], 25 | message=str(tmpfile), 26 | data={}, 27 | ) 28 | 29 | outcome = module.plugin(srv, item) 30 | 31 | assert outcome is True 32 | stdout, stderr = capfd.readouterr() 33 | assert "Hello, world." == stdout 34 | 35 | 36 | def test_execute_failure(srv, caplog, capfd): 37 | """ 38 | Dispatch a single command invocation with an unknown command, and verify the failure will be logged. 39 | """ 40 | 41 | module = mqttwarn.services.execute 42 | 43 | item = Item( 44 | target="test", 45 | addrs=["foobar"], 46 | message="", 47 | data={}, 48 | ) 49 | 50 | outcome = module.plugin(srv, item) 51 | 52 | assert outcome is False 53 | if sys.platform == "win32": 54 | assert "Cannot execute ['foobar'] because [WinError 2] The system cannot find the file specified" in caplog.text 55 | else: 56 | assert "Cannot execute ['foobar'] because [Errno 2] No such file or directory: 'foobar'" in caplog.text 57 | -------------------------------------------------------------------------------- /tests/services/test_http.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2023 The mqttwarn developers 3 | from mqttwarn.configuration import Config 4 | from mqttwarn.core import bootstrap, load_services 5 | 6 | 7 | def test_http_urllib_load_by_alias(caplog): 8 | """ 9 | Verify loading the `http` service works, even if its implementation module is called `http_urllib`. 10 | """ 11 | 12 | config = Config() 13 | config.add_section("config:http") 14 | bootstrap(config=config) 15 | load_services(["http"]) 16 | -------------------------------------------------------------------------------- /tests/services/test_log.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2022 The mqttwarn developers 3 | import json 4 | 5 | from tests import configfile_full 6 | from tests.util import core_bootstrap, send_message 7 | 8 | 9 | def test_log_invalid_target(caplog): 10 | """ 11 | Verify that mqttwarn warns appropriately when evaluating 12 | topic target interpolating yields an invalid log level. 13 | """ 14 | 15 | # Bootstrap the core machinery without MQTT. 16 | core_bootstrap(configfile=configfile_full) 17 | 18 | # Signal mocked MQTT message to the core machinery for processing. 19 | payload = json.dumps({"loglevel": "invalid", "message": "Foo bar"}) 20 | send_message(topic="test/targets-interpolated", payload=payload) 21 | 22 | # Proof that the message has been routed to the "log" plugin properly. 23 | assert ("mqttwarn.core", 20, "Invoking service plugin for `log'") in caplog.record_tuples 24 | assert ( 25 | "mqttwarn.services.log", 26 | 40, 27 | "Cannot invoke service log with level `invalid': 'invalid'", 28 | ) in caplog.record_tuples 29 | assert ( 30 | "mqttwarn.core", 31 | 30, 32 | "Notification failed or timed out. service=log, topic=test/targets-interpolated", 33 | ) in caplog.record_tuples 34 | 35 | 36 | def test_log_broken_target(caplog): 37 | """ 38 | Verify that mqttwarn warns appropriately when evaluating 39 | topic target interpolating does not yield a list. 40 | """ 41 | 42 | # Bootstrap the core machinery without MQTT. 43 | core_bootstrap(configfile=configfile_full) 44 | 45 | # Signal mocked MQTT message to the core machinery for processing. 46 | payload = json.dumps({"loglevel": "broken", "message": "Foo bar"}) 47 | send_message(topic="test/targets-interpolated", payload=payload) 48 | 49 | # Proof that the message has been routed to the "log" plugin properly. 50 | assert ("mqttwarn.core", 20, "Invoking service plugin for `log'") in caplog.record_tuples 51 | assert ( 52 | "mqttwarn.core", 53 | 40, 54 | "Invoking service failed. Reason: `item.addrs` is not a list. service=log, topic=test/targets-interpolated", 55 | ) in caplog.record_tuples 56 | assert ( 57 | "mqttwarn.core", 58 | 30, 59 | "Notification failed or timed out. service=log, topic=test/targets-interpolated", 60 | ) in caplog.record_tuples 61 | -------------------------------------------------------------------------------- /tests/services/test_noop.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2023 The mqttwarn developers 3 | from mqttwarn.model import ProcessorItem 4 | from mqttwarn.util import load_module_by_name 5 | 6 | 7 | def test_noop_success(srv, caplog): 8 | module = load_module_by_name("mqttwarn.services.noop") 9 | 10 | item = ProcessorItem() 11 | outcome = module.plugin(srv, item) 12 | 13 | assert outcome is True 14 | assert "Successfully sent message using noop" in caplog.messages 15 | 16 | 17 | def test_noop_failure(srv, caplog): 18 | module = load_module_by_name("mqttwarn.services.noop") 19 | 20 | item = ProcessorItem(message="fail") 21 | outcome = module.plugin(srv, item) 22 | 23 | assert outcome is False 24 | assert "Failed sending message using noop" in caplog.messages 25 | -------------------------------------------------------------------------------- /tests/test_cron.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock, call 2 | 3 | from mqttwarn.cron import PeriodicThread 4 | from tests.util import delay 5 | 6 | 7 | def test_periodic_thread_success_now(): 8 | """ 9 | Proof that the `cron.PeriodicThread` implementation works as intended. 10 | """ 11 | callback = Mock() 12 | pt = PeriodicThread(callback=callback, period=0.05, name="foo", srv="SRVDUMMY", now=True, options={"foo": "bar"}) 13 | pt.start() 14 | delay(0.35) 15 | pt.cancel() 16 | pt.join() 17 | 18 | # Timer should have been called at least two times. 19 | assert [ 20 | call("SRVDUMMY", options={"foo": "bar"}), 21 | call("SRVDUMMY", options={"foo": "bar"}), 22 | ] in callback.mock_calls 23 | 24 | 25 | def test_periodic_thread_success_not_now(): 26 | """ 27 | Proof that the `cron.PeriodicThread` implementation works as intended. 28 | """ 29 | callback = Mock() 30 | pt = PeriodicThread(callback=callback, period=0.05, name="foo", srv="SRVDUMMY", now=False, options={"foo": "bar"}) 31 | pt.start() 32 | delay(0.35) 33 | pt.cancel() 34 | pt.join() 35 | 36 | # Timer should have been called at least two times. 37 | assert [ 38 | call("SRVDUMMY", options={"foo": "bar"}), 39 | call("SRVDUMMY", options={"foo": "bar"}), 40 | ] in callback.mock_calls 41 | 42 | 43 | def test_periodic_thread_failure(caplog): 44 | """ 45 | Proof that the `cron.PeriodicThread` implementation croaks as intended. 46 | """ 47 | 48 | def callback(srv, *args, **kwargs): 49 | assert srv == "SRVDUMMY" 50 | assert args == () 51 | assert kwargs == {"options": {"foo": "bar"}} 52 | raise ValueError("Something failed") 53 | 54 | pt = PeriodicThread(callback=callback, period=0.005, name="foo", srv="SRVDUMMY", now=True, options={"foo": "bar"}) 55 | pt.start() 56 | delay(0.0125) 57 | pt.cancel() 58 | pt.join() 59 | 60 | assert "Exception while running periodic thread 'foo'" in caplog.messages 61 | assert "ValueError: Something failed" in caplog.text 62 | -------------------------------------------------------------------------------- /tests/test_model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2018-2022 The mqttwarn developers 3 | from copy import deepcopy 4 | 5 | from mqttwarn.core import make_service 6 | from mqttwarn.model import Job, ProcessorItem, Struct 7 | 8 | JOB_PRIO1 = dict( 9 | prio=1, service="service", section="section", topic="topic", payload="payload", data="data", target="target" 10 | ) 11 | JOB_PRIO2 = dict( 12 | prio=2, service="service", section="section", topic="topic", payload="payload", data="data", target="target" 13 | ) 14 | JOB_PRIO1_COPY = deepcopy(JOB_PRIO1) 15 | 16 | 17 | def test_make_service(): 18 | """ 19 | Verify creation of `Service` instance. 20 | """ 21 | service = make_service(name="foo") 22 | assert "" 62 | assert struct.enum() == data 63 | 64 | 65 | def test_processoritem(): 66 | item = ProcessorItem() 67 | assert item.asdict() == { 68 | "service": None, 69 | "target": None, 70 | "config": {}, 71 | "addrs": [], 72 | "priority": None, 73 | "section": None, 74 | "topic": None, 75 | "title": None, 76 | "message": None, 77 | "data": None, 78 | } 79 | assert item.get("foo") is None 80 | -------------------------------------------------------------------------------- /tests/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # (c) 2018-2021 The mqttwarn developers 3 | import shlex 4 | import threading 5 | import time 6 | from unittest.mock import patch 7 | 8 | import paho 9 | from paho.mqtt.client import MQTTMessage 10 | 11 | import mqttwarn 12 | from mqttwarn.commands import run as run_command 13 | from mqttwarn.configuration import load_configuration 14 | from mqttwarn.core import bootstrap, load_services, on_message, start_workers 15 | 16 | 17 | def core_bootstrap(configfile=None): 18 | """ 19 | Bootstrap the core machinery without MQTT. 20 | """ 21 | 22 | # If mqttwarn was already invoked beforehand, reset "exit flag". 23 | # TODO: Get rid of global variables. 24 | mqttwarn.core.exit_flag = False 25 | 26 | # Load configuration file 27 | config = load_configuration(configfile) 28 | 29 | # Bootstrap mqttwarn.core 30 | bootstrap(config=config, scriptname="testdrive") 31 | 32 | # Load services 33 | services = config.getlist("defaults", "launch") 34 | load_services(services) 35 | 36 | # Launch worker threads to operate on queue 37 | start_workers() 38 | 39 | 40 | def send_message(topic=None, payload=None, retain=False): 41 | 42 | # Mock an instance of an Eclipse Paho MQTTMessage 43 | message = MQTTMessage(mid=42, topic=topic.encode("utf-8")) 44 | if payload is not None: 45 | message.payload = payload.encode("utf-8") 46 | if retain: 47 | message.retain = True 48 | 49 | # Signal the message to the machinery 50 | on_message(None, None, message) 51 | 52 | # Give the machinery some time to process the message 53 | delay() 54 | 55 | 56 | def delay(seconds=0.075): 57 | """ 58 | Wait for designated number of seconds. 59 | """ 60 | threading.Event().wait(seconds) 61 | 62 | 63 | def mqtt_process(mqttc: paho.mqtt.client.Client, loops=2): 64 | """ 65 | Process network events for Paho MQTT client library. Wait a bit before and after. 66 | """ 67 | delay() 68 | for _ in range(loops): 69 | mqttc.loop(max_packets=10) 70 | time.sleep(0.01) 71 | delay() 72 | 73 | 74 | def invoke_command(capfd, command): 75 | if not isinstance(command, list): 76 | command = shlex.split(command) 77 | with patch("sys.argv", command): 78 | run_command() 79 | output = capfd.readouterr() 80 | return output.out, output.err 81 | 82 | 83 | class FakeResponse: 84 | """ 85 | https://www.mitchellcurrie.com/blog-post/python-mock-unittesting/ 86 | """ 87 | 88 | status: int 89 | data: bytes 90 | 91 | def __init__(self, *, data: bytes): 92 | self.data = data 93 | 94 | def read(self): 95 | self.status = 200 if self.data is not None else 404 96 | return self.data 97 | 98 | def close(self): 99 | pass 100 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # content of: tox.ini , put in same dir as setup.py 2 | [tox] 3 | envlist = py{27,36,37,38,39},pypy,pypy3 4 | 5 | [testenv] 6 | # install pytest in the virtualenv where commands will be executed 7 | deps = pytest 8 | commands = 9 | # NOTE: you can run any command line tool here - not just tests 10 | pytest 11 | --------------------------------------------------------------------------------