From c05bf7eb4f61c4a285193236a998281662151361 Mon Sep 17 00:00:00 2001 From: Mike Bloy Date: Sun, 28 Feb 2021 14:55:16 -0600 Subject: [PATCH 1/7] move dev packages to tox.ini --- setup.cfg | 21 --------------------- tox.ini | 43 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/setup.cfg b/setup.cfg index cb55349..adcbe17 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,26 +19,5 @@ packages = find: install_requires = paho-mqtt -[options.extras_require] -dev = - bpython - flake8 - flake8-bandit - flake8-blind-except - flake8-builtins - flake8-docstrings - flake8-executable - flake8-isort - flake8-logging-format - mypy - pdbpp - pycodestyle - pydocstyle - pytest - pytest-cov - python-language-server - rope - sphinx - [options.packages.find] where=src diff --git a/tox.ini b/tox.ini index 14ebee4..b07a242 100644 --- a/tox.ini +++ b/tox.ini @@ -5,27 +5,43 @@ isolated_build = True [testenv] wheel = true wheel_build_env = build -extras= - dev [testenv:build] deps = setuptools - [testenv:py37] +deps = + pytest + pytest-cov + pdbpp commands = pytest --cov={envsitepackagesdir}/hasskiosk \ --cov-report=term-missing \ - --cov-report=xml:coverage.xml \ - --junitxml=test-report.xml \ + # --cov-report=xml:coverage.xml \ + # --junitxml=test-report.xml \ --cov-branch \ - --cov-fail-under=80 + --cov-fail-under=80 \ + {posargs} [testenv:security] +deps = + bandit commands = bandit {envsitepackagesdir}/hasskiosk -r [testenv:lint] +deps = + flake8 + flake8-bandit + flake8-blind-except + flake8-builtins + flake8-docstrings + flake8-executable + flake8-isort + flake8-logging-format + flake8-mypy + pycodestyle + pydocstyle commands = flake8 --output-file pylint-out.txt --format pylint --tee @@ -33,6 +49,21 @@ commands = commands = python setup.py sdist bdist_wheel +[testenv:dev] +# this environment lets tox create a development env quickly and easily +deps = + bpython + mypy + python-language-server + rope + {[testenv:py37]deps} + {[testenv:security]deps} + {[testenv:security]deps} + {[testenv:lint]deps} +basepython = python3.7 +envdir = {toxinidir}/.venv +usedevelop = True + [pycodestyle] max-line-length = 87 -- 2.30.2 From f6cbdc9da737ed2786375fd608aaeeae2cfc6ad8 Mon Sep 17 00:00:00 2001 From: Mike Bloy Date: Sun, 28 Feb 2021 15:37:43 -0600 Subject: [PATCH 2/7] basic test, fix some typos --- setup.cfg | 3 ++- tests/conftest.py | 1 + tests/test_version.py | 10 ++++++++++ tox.ini | 6 +++--- 4 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/test_version.py diff --git a/setup.cfg b/setup.cfg index adcbe17..a7db1ae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ # https://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files [metadata] -name = hasskisok +name = hasskiosk author = Mike Bloy author_email = mike@bloy.org description = Helper application for homeassistant kiosk screens @@ -17,6 +17,7 @@ package_dir = =src packages = find: install_requires = + environs paho-mqtt [options.packages.find] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..b97cb1e --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1 @@ +"""Test configuration and fixtures.""" diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..e8b4e3f --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,10 @@ +"""Test version number handling.""" + +from importlib_metadata import version + +from hasskiosk import __version__ + + +def test_package_version_matches_dunder_version(): + """Test that package metadata version matches the package __version__.""" + assert __version__ == version("hasskiosk") diff --git a/tox.ini b/tox.ini index b07a242..afd9a9a 100644 --- a/tox.ini +++ b/tox.ini @@ -5,12 +5,14 @@ isolated_build = True [testenv] wheel = true wheel_build_env = build +basepython = python3.7 [testenv:build] deps = setuptools [testenv:py37] deps = + importlib_metadata pytest pytest-cov pdbpp @@ -32,14 +34,12 @@ commands = [testenv:lint] deps = flake8 - flake8-bandit flake8-blind-except flake8-builtins flake8-docstrings flake8-executable flake8-isort flake8-logging-format - flake8-mypy pycodestyle pydocstyle commands = @@ -51,6 +51,7 @@ commands = [testenv:dev] # this environment lets tox create a development env quickly and easily +recreate=True deps = bpython mypy @@ -60,7 +61,6 @@ deps = {[testenv:security]deps} {[testenv:security]deps} {[testenv:lint]deps} -basepython = python3.7 envdir = {toxinidir}/.venv usedevelop = True -- 2.30.2 From 20b92734e174f16ca6b5b8b238484bba6284518f Mon Sep 17 00:00:00 2001 From: Mike Bloy Date: Sun, 28 Feb 2021 17:53:08 -0600 Subject: [PATCH 3/7] create configuration reader --- src/hasskiosk/config.py | 68 ++++++++++++++++++++++++++++++++++ tests/test_config.py | 81 +++++++++++++++++++++++++++++++++++++++++ tox.ini | 2 +- 3 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 src/hasskiosk/config.py create mode 100644 tests/test_config.py diff --git a/src/hasskiosk/config.py b/src/hasskiosk/config.py new file mode 100644 index 0000000..92e3c49 --- /dev/null +++ b/src/hasskiosk/config.py @@ -0,0 +1,68 @@ +"""Configuration management from environment.""" + +import logging.config +from logging import getLogger +from typing import Any, Dict + +from environs import Env + +from ._version import version + + +def read_config() -> Dict[str, Any]: + """Read the configuration from the environment.""" + env = Env() + env.read_env() + with env.prefixed("HASSKIOSK_"): + config = { + "sysname": env("SYSTEM_NAME", "hasskiosk"), + "version": version, + "logging": { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "simple": { + "format": "%(asctime)s %(levelname)s (%(name)s) %(message)s", + "converter": "time.gmtime", + }, + }, + "handlers": { + "stdout": { + "class": "logging.StreamHandler", + "level": env("LOG_LEVEL", "INFO"), + "formatter": "simple", + "stream": "ext://sys.stdout", + }, + }, + "loggers": { + "hasskiosk": { + "level": "DEBUG", + "propagate": "yes", + }, + }, + "root": { + "level": "WARN", + "handlers": ["stdout"], + }, + }, + } + with env.prefixed("TOPIC_"): + config["topics"] = { + "presence": env("PRESENCE"), + } + with env.prefixed("MQTT_"): + config["mqtt"] = { + "host": env("HOST"), + "port": env.int("PORT", 1883), + "username": env("USERNAME"), + "password": env("PASSWORD"), + "keepalive": env.int("KEEPALIVE", 60), + } + return config + + +def configure_logging(config: Dict[str, Any]) -> None: + """Configure logging using the logging key from the configuration.""" + logging.config.dictConfig(config["logging"]) + log = getLogger(__name__) + log.info("%s v%s starting up.", config["sysname"], config["version"]) diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..0b0fb56 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,81 @@ +"""Tests for the configuration management.""" + +import logging +from typing import Any, Dict + +import pytest + +from hasskiosk import __version__ +from hasskiosk.config import configure_logging, read_config + + +@pytest.fixture(autouse=True) +def mock_env(monkeypatch): + """Environment mock to test values.""" + monkeypatch.setenv("HASSKIOSK_TOPIC_PRESENCE", "home/test/presence") + monkeypatch.setenv("HASSKIOSK_MQTT_HOST", "ha.example.com") + monkeypatch.setenv("HASSKIOSK_MQTT_USERNAME", "testymctesterson") + monkeypatch.setenv("HASSKIOSK_MQTT_PASSWORD", "hunter2") + + +@pytest.fixture +def logging_config() -> Dict[str, Any]: + """logging configuration fixture.""" + config = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "simple": { + "format": "%(asctime)s %(levelname)s (%(name)s) %(message)s", + "converter": "time.gmtime", + }, + }, + "handlers": { + "stdout": { + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "simple", + "stream": "ext://sys.stdout", + }, + }, + "loggers": { + "hasskiosk": { + "level": "DEBUG", + "propagate": "yes", + }, + }, + "root": { + "level": "WARN", + "handlers": ["stdout"], + }, + } + return config + + +def test_read_config(): + """Test the read_config function.""" + config = read_config() + assert config["version"] == __version__ + assert config["sysname"] == "hasskiosk" + assert config["mqtt"] == { + "host": "ha.example.com", + "username": "testymctesterson", + "password": "hunter2", + "port": 1883, + "keepalive": 60, + } + + +def test_configure_logging(logging_config): + """Test the logging configuration.""" + config = { + "version": "1.2.3.test", + "sysname": "testkiosk", + "logging": logging_config, + } + configure_logging(config) + log = logging.getLogger(__name__) + rootlog = log + while rootlog.parent: + rootlog = rootlog.parent + assert len(rootlog.handlers) == 1 diff --git a/tox.ini b/tox.ini index afd9a9a..aed074b 100644 --- a/tox.ini +++ b/tox.ini @@ -61,7 +61,7 @@ deps = {[testenv:security]deps} {[testenv:security]deps} {[testenv:lint]deps} -envdir = {toxinidir}/.venv +#envdir = {toxinidir}/.venv usedevelop = True [pycodestyle] -- 2.30.2 From bed27b83dc8dfb6d37015bfad856830d892c6fc3 Mon Sep 17 00:00:00 2001 From: Mike Bloy Date: Sun, 28 Feb 2021 23:48:35 -0600 Subject: [PATCH 4/7] add working proof of concept using asyncio-mqtt --- setup.cfg | 2 +- src/hasskiosk/__main__.py | 6 +++ src/hasskiosk/config.py | 12 ++++-- src/hasskiosk/mqtt.py | 74 ++++++++++++++++++++++++++++++++++ src/hasskiosk/runner.py | 84 +++++++++++++++++++++++++++++++++++++++ tests/test_config.py | 9 ++++- tox.ini | 1 - 7 files changed, 181 insertions(+), 7 deletions(-) create mode 100644 src/hasskiosk/__main__.py create mode 100644 src/hasskiosk/mqtt.py create mode 100644 src/hasskiosk/runner.py diff --git a/setup.cfg b/setup.cfg index a7db1ae..7ab2a3e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,8 +17,8 @@ package_dir = =src packages = find: install_requires = + asyncio_mqtt environs - paho-mqtt [options.packages.find] where=src diff --git a/src/hasskiosk/__main__.py b/src/hasskiosk/__main__.py new file mode 100644 index 0000000..57e2306 --- /dev/null +++ b/src/hasskiosk/__main__.py @@ -0,0 +1,6 @@ +"""Main module runner.""" + +from .runner import run + +if __name__ == "__main__": + run() diff --git a/src/hasskiosk/config.py b/src/hasskiosk/config.py index 92e3c49..6d2e646 100644 --- a/src/hasskiosk/config.py +++ b/src/hasskiosk/config.py @@ -1,6 +1,8 @@ """Configuration management from environment.""" import logging.config +import os +import socket from logging import getLogger from typing import Any, Dict @@ -46,10 +48,6 @@ def read_config() -> Dict[str, Any]: }, }, } - with env.prefixed("TOPIC_"): - config["topics"] = { - "presence": env("PRESENCE"), - } with env.prefixed("MQTT_"): config["mqtt"] = { "host": env("HOST"), @@ -57,7 +55,13 @@ def read_config() -> Dict[str, Any]: "username": env("USERNAME"), "password": env("PASSWORD"), "keepalive": env.int("KEEPALIVE", 60), + "subscription": env("SUBSCRIBE_TOPIC"), + "screen_state_topic": env.str("SCREEN_STATE_TOPIC", "home/+/presence"), } + hostname = socket.gethostname() + pid = os.getpid() + sysname = config["sysname"] + config["client_id"] = f"{sysname}-{hostname}-{pid}" return config diff --git a/src/hasskiosk/mqtt.py b/src/hasskiosk/mqtt.py new file mode 100644 index 0000000..2fbbba0 --- /dev/null +++ b/src/hasskiosk/mqtt.py @@ -0,0 +1,74 @@ +"""Manage mqtt connections.""" + +import asyncio +import logging +from typing import Any, Dict + +from gmqtt import Client + + +class MQTT: + """MQTT manager. Wrapper around paho.mqtt.client.Client.""" + + def __init__(self, config: Dict[str, Any]): + """Init MQTT. + + Arguments: + config: a config object returned by config.read_config() + """ + mqtt = config["mqtt"] + self._host = mqtt["host"] + self._port = mqtt["port"] + self._keepalive = mqtt["keepalive"] + self._username = mqtt["username"] + self._password = mqtt["password"] + self._client_id = config["client_id"] + self._topics: Dict[str, Any] = dict() + self._subscriptions = mqtt["subscriptions"] + self._client = Client(client_id=self._client_id, clean_session=True) + + async def connect(self): + """Connect to the client and log the connection.""" + logger = logging.getLogger(__name__) + logger.info( + "connecting to MQTT at %s:%s with client_id %s", + self._host, + self._port, + self._client_id, + ) + self._client.set_auth_credentials(self._username, self._password) + self._client.on_connect = self.on_connect + self._client.on_disconnect = self.on_disconnect + self._client.on_subscribe = self.on_subscribe + self._client.on_message = self.on_message + await self._client.connect(self._host, self._port, self._keepalive) + + async def disconnect(self): + """Wrapper around client disconnect.""" + await self._client.disconnect() + + def on_connect(self, client: Client, flags: int, rc: int): + """Callback method for the client connection.""" + logger = logging.getLogger(__name__) + logger.info("client %s connected with result code %s", client, rc) + for sub in self._subscriptions: + client.subscribe(sub, qos=0) + + @staticmethod + def on_disconnect(client: Client, packet, exc=None): + """Callback for disconnections.""" + logger = logging.getLogger(__name__) + logger.info("disconnected from broker: %s", client) + + @staticmethod + def on_subscribe(client: Client, mid: int, qos: int, properties): + """Callback for subscriptions.""" + logger = logging.getLogger(__name__) + logger.info("Subscribed to topic(s) with mid %s and qos %s", mid, qos) + + @staticmethod + def on_message(client: Client, topic, payload: bytes, qos, properties): + """Callback for message handling.""" + logger = logging.getLogger(__name__) + message = payload.decode() + logger.info("Recieved message '%s' on topic %s", message, topic) diff --git a/src/hasskiosk/runner.py b/src/hasskiosk/runner.py new file mode 100644 index 0000000..901e5b3 --- /dev/null +++ b/src/hasskiosk/runner.py @@ -0,0 +1,84 @@ +"""Runner and daemon management.""" + +import asyncio +import logging +from contextlib import AsyncExitStack +from typing import Any, Dict, Set + +from asyncio_mqtt import Client + +from .config import configure_logging, read_config + + +def run(): + """Run the daemon.""" + config = read_config() + configure_logging(config) + asyncio.run(main(config)) + + +async def main(config: Dict[str, Any]): + """Setup and run the async tasks.""" + async with AsyncExitStack() as exit_stack: + tasks: Set[asyncio.Task] = set() + exit_stack.push_async_callback(cancel_tasks, tasks) + + mqtt = Client( + hostname=config["mqtt"]["host"], + port=config["mqtt"]["port"], + username=config["mqtt"]["username"], + password=config["mqtt"]["password"], + clean_session=True, + ) + await exit_stack.enter_async_context(mqtt) + + topic_handlers = ( + (config["mqtt"]["screen_state_topic"], screen_state_mqtt_handler), + ) + for topic, handler in topic_handlers: + manager = mqtt.filtered_messages(topic) + messages = await exit_stack.enter_async_context(manager) + tasks.add(asyncio.create_task(handler(messages))) + + other_messages = await exit_stack.enter_async_context( + mqtt.unfiltered_messages() + ) + tasks.add(asyncio.create_task(dead_letter_handler(other_messages))) + + await mqtt.subscribe(config["mqtt"]["subscription"]) + + await asyncio.gather(*tasks) + + +async def screen_state_mqtt_handler(messages): + """Screen state handler, reacts on presence messages.""" + log = logging.getLogger(__name__) + async for message in messages: + log.info( + "screen sate message on topic %s: %s", + message.topic, + message.payload.decode(), + ) + + +async def dead_letter_handler(messages): + """Logger for uncaught messages.""" + log = logging.getLogger(__name__) + async for message in messages: + log.info( + "unfiltered message on topic %s: %s", + message.topic, + message.payload.decode(), + ) + + +async def cancel_tasks(tasks): + """Cancel tasks on shutdown.""" + for task in tasks: + if task.done(): + continue + task.cancel() + try: + await task + except asyncio.CancelledError: + pass diff --git a/tests/test_config.py b/tests/test_config.py index 0b0fb56..a0d6633 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,6 +1,8 @@ """Tests for the configuration management.""" import logging +import os +import socket from typing import Any, Dict import pytest @@ -12,7 +14,7 @@ from hasskiosk.config import configure_logging, read_config @pytest.fixture(autouse=True) def mock_env(monkeypatch): """Environment mock to test values.""" - monkeypatch.setenv("HASSKIOSK_TOPIC_PRESENCE", "home/test/presence") + monkeypatch.setenv("HASSKIOSK_MQTT_SUBSCRIBE_TOPIC", "home/test/#") monkeypatch.setenv("HASSKIOSK_MQTT_HOST", "ha.example.com") monkeypatch.setenv("HASSKIOSK_MQTT_USERNAME", "testymctesterson") monkeypatch.setenv("HASSKIOSK_MQTT_PASSWORD", "hunter2") @@ -55,14 +57,19 @@ def logging_config() -> Dict[str, Any]: def test_read_config(): """Test the read_config function.""" config = read_config() + hostname = socket.gethostname() + pid = os.getpid() assert config["version"] == __version__ assert config["sysname"] == "hasskiosk" + assert config["client_id"] == f"hasskiosk-{hostname}-{pid}" assert config["mqtt"] == { "host": "ha.example.com", "username": "testymctesterson", "password": "hunter2", "port": 1883, "keepalive": 60, + "subscription": "home/test/#", + "screen_state_topic": "#/presence", } diff --git a/tox.ini b/tox.ini index aed074b..89cb415 100644 --- a/tox.ini +++ b/tox.ini @@ -54,7 +54,6 @@ commands = recreate=True deps = bpython - mypy python-language-server rope {[testenv:py37]deps} -- 2.30.2 From 0063951aeba6cdcc747f4487affc1506894ada3a Mon Sep 17 00:00:00 2001 From: Mike Bloy Date: Sun, 7 Mar 2021 13:45:31 -0600 Subject: [PATCH 5/7] add use of pip-tools to pin requirements --- requirements.txt | 16 ++++++++++++++++ tox.ini | 2 ++ 2 files changed, 18 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..97bbb8a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,16 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile +# +asyncio-mqtt==0.8.1 + # via hasskiosk (setup.py) +environs==9.3.1 + # via hasskiosk (setup.py) +marshmallow==3.10.0 + # via environs +paho-mqtt==1.5.1 + # via asyncio-mqtt +python-dotenv==0.15.0 + # via environs diff --git a/tox.ini b/tox.ini index 89cb415..75b6978 100644 --- a/tox.ini +++ b/tox.ini @@ -56,6 +56,8 @@ deps = bpython python-language-server rope + pip-tools + -r{toxinidir}/requirements.txt {[testenv:py37]deps} {[testenv:security]deps} {[testenv:security]deps} -- 2.30.2 From 0a29d2bad35b87888fc6569e9ea37efc73cb4308 Mon Sep 17 00:00:00 2001 From: Mike Bloy Date: Sun, 7 Mar 2021 13:47:30 -0600 Subject: [PATCH 6/7] create basic handler --- setup.cfg | 4 ++ src/hasskiosk/__init__.py | 2 + src/hasskiosk/mqtt.py | 102 +++++++++++++++----------------------- src/hasskiosk/runner.py | 16 ------ tests/test_config.py | 4 +- tox.ini | 1 + 6 files changed, 49 insertions(+), 80 deletions(-) diff --git a/setup.cfg b/setup.cfg index 7ab2a3e..1e80a9f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,3 +22,7 @@ install_requires = [options.packages.find] where=src + +[options.entry_points] +console_scripts = + hasskiosk = hasskiosk:run diff --git a/src/hasskiosk/__init__.py b/src/hasskiosk/__init__.py index 70337f3..ec1b423 100644 --- a/src/hasskiosk/__init__.py +++ b/src/hasskiosk/__init__.py @@ -1,7 +1,9 @@ """Home Assistant Kiosk.""" from ._version import version as __version__ +from .runner import run __all__ = [ + "run", "__version__", ] diff --git a/src/hasskiosk/mqtt.py b/src/hasskiosk/mqtt.py index 2fbbba0..b295901 100644 --- a/src/hasskiosk/mqtt.py +++ b/src/hasskiosk/mqtt.py @@ -1,74 +1,52 @@ """Manage mqtt connections.""" import asyncio -import logging -from typing import Any, Dict +from contextlib import AsyncExitStack +from typing import Set -from gmqtt import Client +from asyncio_mqtt import Client -class MQTT: - """MQTT manager. Wrapper around paho.mqtt.client.Client.""" +class MQTTManager: + """MQTT manager class.""" - def __init__(self, config: Dict[str, Any]): - """Init MQTT. + def __init__(self, hostname: str, port: int, username: str, password: str): + """Initialize with the following data. Arguments: - config: a config object returned by config.read_config() + hostname: MQTT host to connect to + port: port to use for connecting + username: authentication username + password: authentication password """ - mqtt = config["mqtt"] - self._host = mqtt["host"] - self._port = mqtt["port"] - self._keepalive = mqtt["keepalive"] - self._username = mqtt["username"] - self._password = mqtt["password"] - self._client_id = config["client_id"] - self._topics: Dict[str, Any] = dict() - self._subscriptions = mqtt["subscriptions"] - self._client = Client(client_id=self._client_id, clean_session=True) + super().__init__() + self._hostname = hostname + self._port = port + self._username = username + self._password = password + self._tasks: Set[asyncio.Task] = set() + self._mqtt: Client = None - async def connect(self): - """Connect to the client and log the connection.""" - logger = logging.getLogger(__name__) - logger.info( - "connecting to MQTT at %s:%s with client_id %s", - self._host, - self._port, - self._client_id, - ) - self._client.set_auth_credentials(self._username, self._password) - self._client.on_connect = self.on_connect - self._client.on_disconnect = self.on_disconnect - self._client.on_subscribe = self.on_subscribe - self._client.on_message = self.on_message - await self._client.connect(self._host, self._port, self._keepalive) + async def run(self): + """MQTT async runner.""" + async with AsyncExitStack() as ctx: + self._tasks = set() + ctx.push_async_callback(self._cancel_tasks) + self._mqtt = Client( + hostname=self._host, + port=self._port, + username=self._username, + password=self._password, + clean_session=True, + ) + await self.enter_async_context(self._mqtt) - async def disconnect(self): - """Wrapper around client disconnect.""" - await self._client.disconnect() - - def on_connect(self, client: Client, flags: int, rc: int): - """Callback method for the client connection.""" - logger = logging.getLogger(__name__) - logger.info("client %s connected with result code %s", client, rc) - for sub in self._subscriptions: - client.subscribe(sub, qos=0) - - @staticmethod - def on_disconnect(client: Client, packet, exc=None): - """Callback for disconnections.""" - logger = logging.getLogger(__name__) - logger.info("disconnected from broker: %s", client) - - @staticmethod - def on_subscribe(client: Client, mid: int, qos: int, properties): - """Callback for subscriptions.""" - logger = logging.getLogger(__name__) - logger.info("Subscribed to topic(s) with mid %s and qos %s", mid, qos) - - @staticmethod - def on_message(client: Client, topic, payload: bytes, qos, properties): - """Callback for message handling.""" - logger = logging.getLogger(__name__) - message = payload.decode() - logger.info("Recieved message '%s' on topic %s", message, topic) + async def _cancel_tasks(self): + for task in self._tasks: + if task.done(): + continue + task.cancel() + try: + await task + except asyncio.CancelledError: + pass diff --git a/src/hasskiosk/runner.py b/src/hasskiosk/runner.py index 901e5b3..8e6b5a1 100644 --- a/src/hasskiosk/runner.py +++ b/src/hasskiosk/runner.py @@ -40,11 +40,6 @@ async def main(config: Dict[str, Any]): messages = await exit_stack.enter_async_context(manager) tasks.add(asyncio.create_task(handler(messages))) - other_messages = await exit_stack.enter_async_context( - mqtt.unfiltered_messages() - ) - tasks.add(asyncio.create_task(dead_letter_handler(other_messages))) - await mqtt.subscribe(config["mqtt"]["subscription"]) await asyncio.gather(*tasks) @@ -61,17 +56,6 @@ async def screen_state_mqtt_handler(messages): ) -async def dead_letter_handler(messages): - """Logger for uncaught messages.""" - log = logging.getLogger(__name__) - async for message in messages: - log.info( - "unfiltered message on topic %s: %s", - message.topic, - message.payload.decode(), - ) - - async def cancel_tasks(tasks): """Cancel tasks on shutdown.""" for task in tasks: diff --git a/tests/test_config.py b/tests/test_config.py index a0d6633..c494b9d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -22,7 +22,7 @@ def mock_env(monkeypatch): @pytest.fixture def logging_config() -> Dict[str, Any]: - """logging configuration fixture.""" + """Logging configuration fixture.""" config = { "version": 1, "disable_existing_loggers": False, @@ -69,7 +69,7 @@ def test_read_config(): "port": 1883, "keepalive": 60, "subscription": "home/test/#", - "screen_state_topic": "#/presence", + "screen_state_topic": "home/+/presence", } diff --git a/tox.ini b/tox.ini index 75b6978..d3eef28 100644 --- a/tox.ini +++ b/tox.ini @@ -103,6 +103,7 @@ include = .tox/**/hasskiosk/ omit = **/hasskiosk/_version.py + **/hasskiosk/__main__.py setup.py tests/* -- 2.30.2 From 519294c19a20e218b615c289dd794d37db9a874a Mon Sep 17 00:00:00 2001 From: Mike Bloy Date: Mon, 8 Mar 2021 08:42:01 -0600 Subject: [PATCH 7/7] created functional version with xset subprocess calls --- src/hasskiosk/runner.py | 19 ++++++++++++++----- tox.ini | 5 +++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/hasskiosk/runner.py b/src/hasskiosk/runner.py index 8e6b5a1..1bc2629 100644 --- a/src/hasskiosk/runner.py +++ b/src/hasskiosk/runner.py @@ -2,6 +2,7 @@ import asyncio import logging +import subprocess from contextlib import AsyncExitStack from typing import Any, Dict, Set @@ -49,11 +50,19 @@ async def screen_state_mqtt_handler(messages): """Screen state handler, reacts on presence messages.""" log = logging.getLogger(__name__) async for message in messages: - log.info( - "screen sate message on topic %s: %s", - message.topic, - message.payload.decode(), - ) + payload = message.payload.decode() + log.info("screen sate message on topic %s: %s", message.topic, payload) + if payload == "on": + cmd = ["xset", "dpms", "force", "on"] + log.info("motion detected, forcing screen state to on. CMD: %s", cmd) + result = subprocess.run(cmd, capture_output=True) + if result.returncode != 0: + log.error( + "nonzero return code: %s. stderr: %r, stdout: %r", + result.returncode, + result.stderr, + result.stdout, + ) async def cancel_tasks(tasks): diff --git a/tox.ini b/tox.ini index d3eef28..3768f51 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,security,lint,bundle +envlist = py37,security,lint isolated_build = True [testenv] @@ -14,6 +14,7 @@ deps = setuptools deps = importlib_metadata pytest + pytest-asyncio pytest-cov pdbpp commands = @@ -29,7 +30,7 @@ commands = deps = bandit commands = - bandit {envsitepackagesdir}/hasskiosk -r + bandit {toxinidir}/src/hasskiosk -r [testenv:lint] deps = -- 2.30.2