1
0
Fork 0

Compare commits

...

6 Commits
0.2 ... master

Author SHA1 Message Date
Dustin 990de7fbb9 sensor: Improve reported value precision
Casting the measured values to `int` loses a lot of precision.  Sending
the raw values, however, results in uselessly over-precise values.
Thus, we need to compromise by truncating the values at the tenths
place.
2021-06-18 18:31:34 -05:00
Dustin 2afae103d9 sensor: Set retain on config/availability messages
When Home Assistant restarts, it puts the sensor in "Unavailable" state
until it receives an "online" message.  Since the sensor has no idea
Home Assistant is waiting for such a message, it will never send one.
By setting the "retain" flag on availability and configuration messages,
the broker will automatically resend them to Home Assistant when it
subscribes to the topic again, which resolves this issue.
2021-06-10 08:51:18 -05:00
Dustin 257bac9d86 sensor: Add device metadata
Associating sensor entities with a device improves organization within
the Home Assistant UI.
2021-06-09 09:04:43 -05:00
Dustin e26363a4c8 sensor: Add unique IDs to sensors
Home Assistant seems to lose data from MQTT sensors that do not have a
unique ID when it restarts.
2021-06-09 09:04:43 -05:00
Dustin e30e8aaedc sensor: Add availability messages
Home Assistant can mark MQTT-based sensors as "unavailable" when it
receives a special message (`offline`) on a designated topic.  We send
an `online` message when the sensor process starts, and `offline` when
the process shuts down.  Additionally, we set the last will and
testament message to `offline` as well, so that Home Assistant will
still get the notification, even if the sensor does not shut down
cleanly.
2021-05-01 15:57:03 -05:00
Dustin 205989aefd sensor: Use pipe to interrupt loop
The `time.sleep` function cannot be stopped prematurely.  This means
that when the program receives a signal indicating it should stop, it
will not be able to do so until the running `sleep` completes.  This
means it could take up to 10 seconds for the process to stop.

A better way to handle the shutdown signal is to use a pipe.  When the
signal is received, the write end of the pipe is closed.  The event loop
will detect this immediately, as `select.select` returns as soon as any
event occurs on any of the polled file descriptors, or the timeout
expires.
2021-05-01 15:27:33 -05:00
2 changed files with 42 additions and 9 deletions

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "thermostat" name = "thermostat"
version = "0.2.0" version = "0.3.0dev2"
description = "" description = ""
authors = ["Dustin C. Hatch <dustin@hatch.name>"] authors = ["Dustin C. Hatch <dustin@hatch.name>"]

View File

@ -1,7 +1,9 @@
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
@ -20,30 +22,45 @@ 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 }}",
}, },
@ -52,18 +69,20 @@ SENSOR_CONFIG = {
class Daemon: class Daemon:
def __init__(self) -> None: def __init__(self) -> None:
self.running = True self.quitpipe = os.pipe()
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")
self.running = False os.close(self.quitpipe[1])
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
@ -73,15 +92,20 @@ 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)
while self.running: self._ready.wait()
while 1:
data = bme280.sample(bus, SENSOR_ADDR, params) data = bme280.sample(bus, SENSOR_ADDR, params)
values = { values = {
"temperature": int(data.temperature), "temperature": adj(data.temperature),
"pressure": int(data.pressure), "pressure": adj(data.pressure),
"humidity": int(data.humidity), "humidity": adj(data.humidity),
} }
client.publish(TOPIC, json.dumps(values)) client.publish(TOPIC, json.dumps(values))
time.sleep(10) ready = select.select((self.quitpipe[0],), (), (), 10)[0]
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()
@ -94,9 +118,14 @@ 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", json.dumps(value) f"homeassistant/sensor/{key}/config",
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")
@ -113,6 +142,10 @@ 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()