1
0
Fork 0

Compare commits

..

No commits in common. "master" and "0.1" have entirely different histories.
master ... 0.1

6 changed files with 11 additions and 234 deletions

View File

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

View File

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

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "thermostat" name = "thermostat"
version = "0.3.0dev2" version = "0.1.0"
description = "" description = ""
authors = ["Dustin C. Hatch <dustin@hatch.name>"] authors = ["Dustin C. Hatch <dustin@hatch.name>"]
@ -9,7 +9,6 @@ python = "^3.7"
smbus2 = "^0.4.1" smbus2 = "^0.4.1"
"RPi.bme280" = "^0.2.3" "RPi.bme280" = "^0.2.3"
paho-mqtt = "^1.5.1" paho-mqtt = "^1.5.1"
DBussy = "^1.3"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
black = "^21.4b2" black = "^21.4b2"

View File

@ -1,169 +0,0 @@
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

@ -1,9 +1,7 @@
import json import json
import logging import logging
import os import os
import select
import signal import signal
import threading
import time import time
from contextlib import closing from contextlib import closing
from types import FrameType from types import FrameType
@ -22,45 +20,30 @@ PORT = 8883
USERNAME = os.environ.get("MQTT_USERNAME", "") USERNAME = os.environ.get("MQTT_USERNAME", "")
PASSWORD = os.environ.get("MQTT_PASSWORD", "") PASSWORD = os.environ.get("MQTT_PASSWORD", "")
TOPIC = "homeassistant/sensor/thermostat" TOPIC = "homeassistant/sensor/thermostat"
AVAILABILITY_TOPIC = f"{TOPIC}/availability"
I2CPORT = 1 I2CPORT = 1
SENSOR_ADDR = 0x77 SENSOR_ADDR = 0x77
DEVICE = {
"manufacturer": "Dustin C. Hatch",
"name": "RPi Thermostat Display",
"model": "RPi Thermostat Display",
"identifiers": [
os.uname().nodename,
],
}
SENSOR_CONFIG = { SENSOR_CONFIG = {
"thermostat_temperature": { "thermostat_temperature": {
"device_class": "temperature", "device_class": "temperature",
"name": "Thermostat Temperature", "name": "Thermostat Temperature",
"device": DEVICE,
"state_topic": TOPIC, "state_topic": TOPIC,
"availability_topic": AVAILABILITY_TOPIC,
"unit_of_measurement": "°C", "unit_of_measurement": "°C",
"value_template": r"{{ value_json.temperature }}", "value_template": r"{{ value_json.temperature }}",
}, },
"thermostat_pressure": { "thermostat_pressure": {
"device_class": "pressure", "device_class": "pressure",
"name": "Thermostat Pressure", "name": "Thermostat Pressure",
"device": DEVICE,
"state_topic": TOPIC, "state_topic": TOPIC,
"availability_topic": AVAILABILITY_TOPIC,
"unit_of_measurement": "hPa", "unit_of_measurement": "hPa",
"value_template": r"{{ value_json.pressure }}", "value_template": r"{{ value_json.pressure }}",
}, },
"thermostat_humidity": { "thermostat_humidity": {
"device_class": "humidity", "device_class": "humidity",
"name": "Thermostat Humidity", "name": "Thermostat Humidity",
"device": DEVICE,
"state_topic": TOPIC, "state_topic": TOPIC,
"availability_topic": AVAILABILITY_TOPIC,
"unit_of_measurement": "%", "unit_of_measurement": "%",
"value_template": r"{{ value_json.humidity }}", "value_template": r"{{ value_json.humidity }}",
}, },
@ -69,20 +52,18 @@ SENSOR_CONFIG = {
class Daemon: class Daemon:
def __init__(self) -> None: def __init__(self) -> None:
self.quitpipe = os.pipe() self.running = True
self._ready = threading.Event()
def on_signal(self, signum: int, frame: FrameType) -> None: def on_signal(self, signum: int, frame: FrameType) -> None:
log.debug("Got signal %d at %s", signum, frame) log.debug("Got signal %d at %s", signum, frame)
log.info("Stopping") log.info("Stopping")
os.close(self.quitpipe[1]) self.running = False
def run(self): def run(self):
signal.signal(signal.SIGINT, self.on_signal) signal.signal(signal.SIGINT, self.on_signal)
signal.signal(signal.SIGTERM, self.on_signal) signal.signal(signal.SIGTERM, self.on_signal)
client = mqtt.Client() client = mqtt.Client()
client.will_set(AVAILABILITY_TOPIC, "offline", retain=True)
client.on_connect = self.on_connect client.on_connect = self.on_connect
client.on_message = self.on_message client.on_message = self.on_message
client.on_disconnect = self.on_disconnect client.on_disconnect = self.on_disconnect
@ -92,20 +73,15 @@ class Daemon:
client.loop_start() client.loop_start()
with closing(smbus2.SMBus(I2CPORT)) as bus: with closing(smbus2.SMBus(I2CPORT)) as bus:
params = bme280.load_calibration_params(bus, SENSOR_ADDR) params = bme280.load_calibration_params(bus, SENSOR_ADDR)
self._ready.wait() while self.running:
while 1:
data = bme280.sample(bus, SENSOR_ADDR, params) data = bme280.sample(bus, SENSOR_ADDR, params)
values = { values = {
"temperature": adj(data.temperature), "temperature": int(data.temperature),
"pressure": adj(data.pressure), "pressure": int(data.pressure),
"humidity": adj(data.humidity), "humidity": int(data.humidity),
} }
client.publish(TOPIC, json.dumps(values)) client.publish(TOPIC, json.dumps(values))
ready = select.select((self.quitpipe[0],), (), (), 10)[0] time.sleep(10)
if self.quitpipe[0] in ready:
os.close(self.quitpipe[0])
break
client.publish(AVAILABILITY_TOPIC, "offline", retain=True)
client.disconnect() client.disconnect()
client.loop_stop() client.loop_stop()
@ -118,14 +94,9 @@ class Daemon:
): ):
log.info("Successfully connected to MQTT broker") log.info("Successfully connected to MQTT broker")
for key, value in SENSOR_CONFIG.items(): for key, value in SENSOR_CONFIG.items():
value["unique_id"] = f"sensor.{key}"
client.publish( client.publish(
f"homeassistant/sensor/{key}/config", f"homeassistant/sensor/{key}/config", json.dumps(value)
json.dumps(value),
retain=True,
) )
client.publish(AVAILABILITY_TOPIC, "online", retain=True)
self._ready.set()
def on_disconnect(self, client, userdata, rc): def on_disconnect(self, client, userdata, rc):
log.error("Lost connection to MQTT broker") log.error("Lost connection to MQTT broker")
@ -142,10 +113,6 @@ class Daemon:
print("Message", client, userdata, msg) print("Message", client, userdata, msg)
def adj(value, p=10):
return int(value * p) / p
def main(): def main():
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
daemon = Daemon() daemon = Daemon()