Compare commits
No commits in common. "main" and "v0.0.0" have entirely different histories.
@ -1,16 +0,0 @@
|
|||||||
#
|
|
||||||
# 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
|
|
||||||
30
setup.cfg
30
setup.cfg
@ -1,7 +1,7 @@
|
|||||||
# https://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files
|
# https://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
name = hasskiosk
|
name = hasskisok
|
||||||
author = Mike Bloy
|
author = Mike Bloy
|
||||||
author_email = mike@bloy.org
|
author_email = mike@bloy.org
|
||||||
description = Helper application for homeassistant kiosk screens
|
description = Helper application for homeassistant kiosk screens
|
||||||
@ -17,12 +17,28 @@ package_dir =
|
|||||||
=src
|
=src
|
||||||
packages = find:
|
packages = find:
|
||||||
install_requires =
|
install_requires =
|
||||||
asyncio_mqtt
|
paho-mqtt
|
||||||
environs
|
|
||||||
|
[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]
|
[options.packages.find]
|
||||||
where=src
|
where=src
|
||||||
|
|
||||||
[options.entry_points]
|
|
||||||
console_scripts =
|
|
||||||
hasskiosk = hasskiosk:run
|
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
"""Home Assistant Kiosk."""
|
"""Home Assistant Kiosk."""
|
||||||
|
|
||||||
from ._version import version as __version__
|
from ._version import version as __version__
|
||||||
from .runner import run
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"run",
|
|
||||||
"__version__",
|
"__version__",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
"""Main module runner."""
|
|
||||||
|
|
||||||
from .runner import run
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run()
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
"""Configuration management from environment."""
|
|
||||||
|
|
||||||
import logging.config
|
|
||||||
import os
|
|
||||||
import socket
|
|
||||||
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("MQTT_"):
|
|
||||||
config["mqtt"] = {
|
|
||||||
"host": env("HOST"),
|
|
||||||
"port": env.int("PORT", 1883),
|
|
||||||
"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
|
|
||||||
|
|
||||||
|
|
||||||
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"])
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
"""Manage mqtt connections."""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from contextlib import AsyncExitStack
|
|
||||||
from typing import Set
|
|
||||||
|
|
||||||
from asyncio_mqtt import Client
|
|
||||||
|
|
||||||
|
|
||||||
class MQTTManager:
|
|
||||||
"""MQTT manager class."""
|
|
||||||
|
|
||||||
def __init__(self, hostname: str, port: int, username: str, password: str):
|
|
||||||
"""Initialize with the following data.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
hostname: MQTT host to connect to
|
|
||||||
port: port to use for connecting
|
|
||||||
username: authentication username
|
|
||||||
password: authentication password
|
|
||||||
"""
|
|
||||||
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 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 _cancel_tasks(self):
|
|
||||||
for task in self._tasks:
|
|
||||||
if task.done():
|
|
||||||
continue
|
|
||||||
task.cancel()
|
|
||||||
try:
|
|
||||||
await task
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
"""Runner and daemon management."""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import subprocess
|
|
||||||
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)))
|
|
||||||
|
|
||||||
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:
|
|
||||||
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):
|
|
||||||
"""Cancel tasks on shutdown."""
|
|
||||||
for task in tasks:
|
|
||||||
if task.done():
|
|
||||||
continue
|
|
||||||
task.cancel()
|
|
||||||
try:
|
|
||||||
await task
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
@ -1 +0,0 @@
|
|||||||
"""Test configuration and fixtures."""
|
|
||||||
@ -1,88 +0,0 @@
|
|||||||
"""Tests for the configuration management."""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import socket
|
|
||||||
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_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")
|
|
||||||
|
|
||||||
|
|
||||||
@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()
|
|
||||||
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": "home/+/presence",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
"""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")
|
|
||||||
50
tox.ini
50
tox.ini
@ -1,48 +1,31 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist = py37,security,lint
|
envlist = py37,security,lint,bundle
|
||||||
isolated_build = True
|
isolated_build = True
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
wheel = true
|
wheel = true
|
||||||
wheel_build_env = build
|
wheel_build_env = build
|
||||||
basepython = python3.7
|
extras=
|
||||||
|
dev
|
||||||
|
|
||||||
[testenv:build]
|
[testenv:build]
|
||||||
deps = setuptools
|
deps = setuptools
|
||||||
|
|
||||||
|
|
||||||
[testenv:py37]
|
[testenv:py37]
|
||||||
deps =
|
|
||||||
importlib_metadata
|
|
||||||
pytest
|
|
||||||
pytest-asyncio
|
|
||||||
pytest-cov
|
|
||||||
pdbpp
|
|
||||||
commands =
|
commands =
|
||||||
pytest --cov={envsitepackagesdir}/hasskiosk \
|
pytest --cov={envsitepackagesdir}/hasskiosk \
|
||||||
--cov-report=term-missing \
|
--cov-report=term-missing \
|
||||||
# --cov-report=xml:coverage.xml \
|
--cov-report=xml:coverage.xml \
|
||||||
# --junitxml=test-report.xml \
|
--junitxml=test-report.xml \
|
||||||
--cov-branch \
|
--cov-branch \
|
||||||
--cov-fail-under=80 \
|
--cov-fail-under=80
|
||||||
{posargs}
|
|
||||||
|
|
||||||
[testenv:security]
|
[testenv:security]
|
||||||
deps =
|
|
||||||
bandit
|
|
||||||
commands =
|
commands =
|
||||||
bandit {toxinidir}/src/hasskiosk -r
|
bandit {envsitepackagesdir}/hasskiosk -r
|
||||||
|
|
||||||
[testenv:lint]
|
[testenv:lint]
|
||||||
deps =
|
|
||||||
flake8
|
|
||||||
flake8-blind-except
|
|
||||||
flake8-builtins
|
|
||||||
flake8-docstrings
|
|
||||||
flake8-executable
|
|
||||||
flake8-isort
|
|
||||||
flake8-logging-format
|
|
||||||
pycodestyle
|
|
||||||
pydocstyle
|
|
||||||
commands =
|
commands =
|
||||||
flake8 --output-file pylint-out.txt --format pylint --tee
|
flake8 --output-file pylint-out.txt --format pylint --tee
|
||||||
|
|
||||||
@ -50,22 +33,6 @@ commands =
|
|||||||
commands =
|
commands =
|
||||||
python setup.py sdist bdist_wheel
|
python setup.py sdist bdist_wheel
|
||||||
|
|
||||||
[testenv:dev]
|
|
||||||
# this environment lets tox create a development env quickly and easily
|
|
||||||
recreate=True
|
|
||||||
deps =
|
|
||||||
bpython
|
|
||||||
python-language-server
|
|
||||||
rope
|
|
||||||
pip-tools
|
|
||||||
-r{toxinidir}/requirements.txt
|
|
||||||
{[testenv:py37]deps}
|
|
||||||
{[testenv:security]deps}
|
|
||||||
{[testenv:security]deps}
|
|
||||||
{[testenv:lint]deps}
|
|
||||||
#envdir = {toxinidir}/.venv
|
|
||||||
usedevelop = True
|
|
||||||
|
|
||||||
[pycodestyle]
|
[pycodestyle]
|
||||||
max-line-length = 87
|
max-line-length = 87
|
||||||
|
|
||||||
@ -104,7 +71,6 @@ include =
|
|||||||
.tox/**/hasskiosk/
|
.tox/**/hasskiosk/
|
||||||
omit =
|
omit =
|
||||||
**/hasskiosk/_version.py
|
**/hasskiosk/_version.py
|
||||||
**/hasskiosk/__main__.py
|
|
||||||
setup.py
|
setup.py
|
||||||
tests/*
|
tests/*
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user