1
0
Fork 0

Compare commits

...

No commits in common. "2ef7f9dd44c7249247a605adfab2bc6eeb084e96" and "a48fd74a153d9926821b8406e6d19131f03b1dd8" have entirely different histories.

7 changed files with 202 additions and 4 deletions

2
.editorconfig Normal file
View File

@ -0,0 +1,2 @@
[*.py]
insert_final_newline = true

View File

@ -1,3 +1,4 @@
{
"python.pythonPath": ".venv/bin/python3.9"
"python.pythonPath": ".venv/bin/python",
"python.formatting.provider": "black"
}

8
backlight.service Normal file
View File

@ -0,0 +1,8 @@
[Unit]
Description=CM3-Panel backlight brightness control service
[Service]
ExecStart=/usr/bin/python3 -m thermostat.backlightd
[Install]
WantedBy=default.target

13
poetry.lock generated
View File

@ -58,6 +58,14 @@ category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "dbussy"
version = "1.3"
description = "language bindings for libdbus, for Python 3.5 or later"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "flake8"
version = "3.9.1"
@ -274,7 +282,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
content-hash = "b45f2d08de21121b2d91ec83f040e748438c74c4d10a1480d56504fc242568da"
content-hash = "8ec699d6ce96b5082d8ff5f190cca908fe5043c6b74fdfbfd825a281ac230264"
[metadata.files]
appdirs = [
@ -297,6 +305,9 @@ colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
dbussy = [
{file = "DBussy-1.3-py35-none-any.whl", hash = "sha256:511cf4c76b9c82fa08075ebee01eb9331fe1277404fab52924a083d9ae3f62b3"},
]
flake8 = [
{file = "flake8-3.9.1-py2.py3-none-any.whl", hash = "sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a"},
{file = "flake8-3.9.1.tar.gz", hash = "sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378"},

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "thermostat"
version = "0.1.0"
version = "0.2.0"
description = ""
authors = ["Dustin C. Hatch <dustin@hatch.name>"]
@ -9,6 +9,7 @@ python = "^3.7"
smbus2 = "^0.4.1"
"RPi.bme280" = "^0.2.3"
paho-mqtt = "^1.5.1"
DBussy = "^1.3"
[tool.poetry.dev-dependencies]
black = "^21.4b2"
@ -22,3 +23,8 @@ build-backend = "poetry.core.masonry.api"
[tool.black]
line-length = 79
[tool.isort]
profile = "black"
multi_line_output = 3
lines_after_imports = 2

169
src/thermostat/backlight.py Normal file
View File

@ -0,0 +1,169 @@
import asyncio
import logging
import os
import signal
import threading
from types import TracebackType
from typing import Optional, Type
import ravel
from RPi import GPIO
log = logging.getLogger("backlightd")
@ravel.interface(
ravel.INTERFACE.SERVER, name="me.dustinhatch.home.thermostat.Backlight"
)
class Backlight:
def __init__(self) -> None:
self.brightness = 100
self.pwm: Optional[GPIO.PWM] = None
def __enter__(self) -> None:
GPIO.setmode(GPIO.BCM)
GPIO.setup(22, GPIO.OUT)
self.pwm = GPIO.PWM(22, 100)
self.pwm.start(0)
def __exit__(
self,
exc_type: Optional[Type[Exception]],
exc_value: Optional[Exception],
tb: Optional[TracebackType],
) -> None:
assert self.pwm
self.pwm.stop()
GPIO.cleanup()
@ravel.method(in_signature="", out_signature="i")
def CurrentBrightness(self) -> int:
return self.brightness
@ravel.method(in_signature="i", out_signature="")
def DecreaseBrightness(self, percent: int = 10) -> None:
if percent < 0 or percent > 100:
raise ValueError(f"Invalid percentage: {percent}")
log.info("Decreasing brightness by %d%%", percent)
self.brightness = max(self.brightness - percent, 0)
@ravel.method(in_signature="i", out_signature="")
def IncreaseBrightness(self, percent: int = 10) -> None:
if percent < 0 or percent > 100:
raise ValueError(f"Invalid percentage: {percent}")
log.info("Increasing brightness by %d%%", percent)
self.brightness = min(self.brightness + percent, 100)
self._set_brightness()
@ravel.method(in_signature="i", out_signature="")
def SetBrightness(self, percent: int) -> None:
if percent < 0 or percent > 100:
raise ValueError(f"Invalid percentage: {percent}")
log.info("Setting brigness to %d%%", percent)
self.brightness = percent
self._set_brightness()
def _set_brightness(self) -> None:
assert self.pwm
log.debug("Setting PWM duty cycle to %d", 100 - self.brightness)
self.pwm.ChangeDutyCycle(100 - self.brightness)
class ScreensaverWatcher:
def __init__(
self, service: Backlight, loop: asyncio.AbstractEventLoop
) -> None:
self.service = service
self.loop = loop
self.done: asyncio.Future
async def run(self) -> None:
assert self.loop
self.done = self.loop.create_future()
cmd = [
"xscreensaver-command",
"-watch",
]
p = await asyncio.create_subprocess_exec(
*cmd,
stdin=asyncio.subprocess.DEVNULL,
stdout=asyncio.subprocess.PIPE,
stderr=None,
loop=self.loop,
)
assert p.stdout
async def process_stdout():
brightness = 100
while 1:
line = await p.stdout.readline()
if not line:
break
event = line.split(None, 1)[0].decode()
if event == "BLANK":
log.debug("Screensaver activated, disabling backlight")
brightness = self.service.CurrentBrightness()
self.service.SetBrightness(0)
elif event == "UNBLANK":
log.debug("Screensaver deactivated, restoring backlight")
self.service.SetBrightness(brightness)
else:
log.warning("Unknown event: %s", line)
task = self.loop.create_task(process_stdout())
await self.done
p.terminate()
await task
def stop(self) -> None:
log.debug("Stopping screensaver watcher")
self.loop.call_soon(self.done.set_result, True)
class Daemon:
def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
self.loop = loop
self.bus: ravel.Connection
self.watcher: ScreensaverWatcher
async def run(self) -> None:
service = Backlight()
self.loop.add_signal_handler(signal.SIGINT, self.stop, signal.SIGINT)
self.loop.add_signal_handler(signal.SIGTERM, self.stop, signal.SIGTERM)
self.bus = await ravel.session_bus_async(self.loop)
self.bus.register(
"/me/dustinhatch/home/thermostat/Backlight",
False,
service,
)
await self.bus.request_name_async(
"me.dustinhatch.home.thermostat.Backlight",
ravel.DBUS.NAME_FLAG_DO_NOT_QUEUE,
)
self.watcher = ScreensaverWatcher(service, self.loop)
with service:
await self.watcher.run()
def stop(self, signum: int) -> None:
log.debug("Got signal %d", signum)
log.info("Shutting down backlight daemon")
self.watcher.stop()
def main() -> None:
os.environ.setdefault("DISPLAY", ":0.0")
logging.basicConfig(level=logging.DEBUG)
loop = asyncio.get_event_loop()
daemon = Daemon(loop)
try:
loop.run_until_complete(daemon.run())
finally:
loop.close()
if __name__ == "__main__":
main()

View File

@ -11,6 +11,7 @@ import bme280
import smbus2
from paho.mqtt import client as mqtt
log = logging.getLogger("sensor")