Skip to content

Commit

Permalink
✨ Add MQTT/Home Assistant support (#14)
Browse files Browse the repository at this point in the history
* Add MQTT support for HA

* Possibly improve handling without paho-mqtt

* Add logging, improve docs

* Log the prefix used, tweak values, minify

* More logging, debugging

For some reason I'm not seeing the status messages coming in, not sure why

* Fix config var

* decode byte string

* Update docs
  • Loading branch information
jerr0328 authored Dec 3, 2023
1 parent c7db35d commit 123a13a
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 10 deletions.
51 changes: 49 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,57 @@ The core logic comes from [this hackaday article](https://hackaday.io/project/53

## Setup

Note this assumes you are running on a Raspberry Pi running Raspberry Pi OS (Buster)
Note this assumes you are running on a Raspberry Pi running Raspberry Pi OS (Bullseye)

1. Install Python 3
2. Install the monitor with `python3 -m pip install co2mini[homekit]` (remove `[homekit]` if you don't use HomeKit)
3. Set up CO2 udev rules by copying `90-co2mini.rules` to `/etc/udev/rules.d/90-co2mini.rules`
4. Set up the service by copying `co2mini.service` to `/etc/systemd/system/co2mini.service`
5. Run `systemctl enable co2mini.service`
5. (Optional) Put a configuration file (see Configuration section below) in `/etc/co2mini.env`
6. Run `systemctl enable co2mini.service`

## Configuration

The `/etc/co2mini.env` file contains the environment variables used to configure co2mini beyond the defaults.
This is mostly necessary when enabling MQTT.

Example:

```bash
MQTT_ENABLED=true
MQTT_BROKER=localhost
```

### MQTT/Home Assistant

The MQTT feature is meant to work with Home Assistant, although nothing bad will happen if you just want to use the MQTT messages directly.

When co2mini starts up, it will send out the discovery message that Home Assistant expects, as well as responding to homeassistant's status when coming online.
Be sure those are enabled in the Home Assistant MQTT integration (usually is enabled by default) if you have any issues.

To configure co2mini, the following environment variables are available:

Variable | Default | Description
------------------------|----------------------|---------------------------------------------------------------------------------------------------------
`NAME` | `co2mini` | This is used for the default display name of the device in Home Assistant
`MQTT_ENABLED` | `False` | Used to enable/disable sending information over MQTT
`MQTT_BROKER` | `localhost` | MQTT Broker hostname
`MQTT_PORT` | `1883` | MQTT broker port number (1883 is the standard MQTT broker port)
`MQTT_USERNAME` | | Username for authenticating to MQTT broker (leave blank if no authentication is needed)
`MQTT_PASSWORD` | | Password for authenticating to MQTT broker (leave blank if no authentication is needed)
`MQTT_DISCOVERY_PREFIX` | `homeassistant` | Prefix for sending MQTT discovery and state messages.
`MQTT_RETAIN_DISCOVERY` | `False` | Flag to enable setting `retain=True` on the discovery messages. You probably don't need this.
`MQTT_OBJECT_ID` | `co2mini_{HOSTNAME}` | Override for setting the `object_id` in Home Assistant. Default builds using the hostname of the device.

### Homekit

If you have the `homekit` dependencies installed, on the first startup you will need to check the logs to get the setup code to integrate with Homekit.
You can find the code using `journalctl -u co2mini.service` or possibly by checking the status with `systemctl status co2mini.service`.

Note also that it's sometimes possible that co2mini will have some errors logged and won't be reporting in Homekit anymore.
If this happens, it seems like the easiest thing to do is to remove the device from your homekit, remove the `accessory.state` file in your home (`rm accessory.state`) and restart `co2mini` (`sudo systemctl restart co2mini.service`) to get a new code to pair.

## Special notes for Dietpi users

- Be sure to install `Python3 pip` as well (ID `130`)
- Make sure the dietpi user is in `plugdev` group (`sudo usermod -aG plugdev dietpi`)
1 change: 1 addition & 0 deletions co2mini.service
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ ExecStart=/home/pi/.local/bin/co2mini /dev/co2mini0
Restart=on-failure
RestartSec=3
Environment=PYTHONUNBUFFERED=1
EnvironmentFile=/etc/co2mini.env

[Install]
WantedBy=multi-user.target
22 changes: 22 additions & 0 deletions co2mini/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import platform

from environs import Env

env = Env()
env.read_env()

HOSTNAME = platform.node()

PROMETHEUS_PORT: int = env.int("CO2_PROMETHEUS_PORT", 9999)
NAME: str = env.str("CO2MINI_NAME", "co2mini")

with env.prefixed("MQTT_"):
MQTT_ENABLED: bool = env.bool("ENABLED", False)
MQTT_BROKER: str = env.str("BROKER", "localhost")
MQTT_PORT: str = env.int("PORT", 1883)
MQTT_USERNAME: str = env.str("USERNAME", "")
MQTT_PASSWORD: str = env.str("PASSWORD", "")
MQTT_DISCOVERY_PREFIX: str = env.str("DISCOVERY_PREFIX", "homeassistant")
MQTT_RETAIN_DISCOVERY: bool = env.bool("RETAIN_DISCOVERY", False)
# Object ID needs to be unique
MQTT_OBJECT_ID: str = env.str("OBJECT_ID", f"co2mini_{HOSTNAME}")
55 changes: 50 additions & 5 deletions co2mini/main.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
#!/usr/bin/env python3

import logging
import os
import sys
from functools import partial

from prometheus_client import Gauge, start_http_server

from . import meter
from . import config, meter

try:
from . import mqtt
except ImportError:

class mqtt:
@staticmethod
def send_co2_value(*args, **kwargs):
pass

@staticmethod
def send_temp_value(*args, **kwargs):
pass

@staticmethod
def get_mqtt_client():
pass

@staticmethod
def start_client(*args, **kwargs):
pass


co2_gauge = Gauge("co2", "CO2 levels in PPM")
temp_gauge = Gauge("temperature", "Temperature in C")

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
PROMETHEUS_PORT = os.getenv("CO2_PROMETHEUS_PORT", 9999)


def co2_callback(sensor, value):
Expand All @@ -23,15 +44,36 @@ def co2_callback(sensor, value):
temp_gauge.set(value)


def generate_callback(mqtt_client):
# TODO: Create the callback, support the loop
co2_handlers = [co2_gauge.set]
temp_handlers = [temp_gauge.set]
if mqtt_client:
co2_handlers.append(partial(mqtt.send_co2_value, mqtt_client))
temp_handlers.append(partial(mqtt.send_temp_value, mqtt_client))

def co2_callback(sensor, value):
if sensor == meter.CO2METER_CO2:
for handler in co2_handlers:
handler(value)
elif sensor == meter.CO2METER_TEMP:
for handler in temp_handlers:
handler(value)

return co2_callback


def main():
# TODO: Better CLI handling
device = sys.argv[1] if len(sys.argv) > 1 else "/dev/co2mini0"
logger.info("Starting with device %s", device)

# Expose metrics
start_http_server(PROMETHEUS_PORT)
start_http_server(config.PROMETHEUS_PORT)

mqtt_client = mqtt.get_mqtt_client() if config.MQTT_ENABLED else None

co2meter = meter.CO2Meter(device=device, callback=co2_callback)
co2meter = meter.CO2Meter(device=device, callback=generate_callback(mqtt_client))
co2meter.start()

try:
Expand All @@ -42,6 +84,9 @@ def main():
except ImportError:
pass

if mqtt_client:
mqtt.start_client(mqtt_client)

# Ensure thread doesn't just end without cleanup
co2meter.join()

Expand Down
83 changes: 83 additions & 0 deletions co2mini/mqtt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import json
import logging

import paho.mqtt.client as mqtt

from . import config

logger = logging.getLogger(__name__)
HA_STATUS_TOPIC = f"{config.MQTT_DISCOVERY_PREFIX}/status"
HA_PREFIX = (
f"{config.MQTT_DISCOVERY_PREFIX}/sensor/{config.HOSTNAME}/{config.MQTT_OBJECT_ID}"
)
HA_TEMP_PREFIX = f"{HA_PREFIX}_temp"
HA_CO2_PREFIX = f"{HA_PREFIX}_co2"
EXPIRE_AFTER_SECONDS = 300


def send_discovery_message(client: mqtt.Client):
logger.info(f"Sending discovery message to mqtt (prefix: {HA_PREFIX})")
device = {"ids": [config.HOSTNAME], "name": config.NAME}
temp_config = {
"dev_cla": "temperature",
"expire_after": EXPIRE_AFTER_SECONDS,
"stat_t": f"{HA_TEMP_PREFIX}/state",
"unit_of_meas": "°C",
"uniq_id": f"{config.MQTT_OBJECT_ID}_T",
"dev": device,
}
co2_config = {
"dev_cla": "carbon_dioxide",
"expire_after": EXPIRE_AFTER_SECONDS,
"stat_t": f"{HA_CO2_PREFIX}/state",
"unit_of_meas": "ppm",
"uniq_id": f"{config.MQTT_OBJECT_ID}_CO2",
"dev": device,
}
client.publish(
f"{HA_TEMP_PREFIX}/config",
json.dumps(temp_config),
retain=config.MQTT_RETAIN_DISCOVERY,
)
client.publish(
f"{HA_CO2_PREFIX}/config",
json.dumps(co2_config),
retain=config.MQTT_RETAIN_DISCOVERY,
)


def on_connect(client: mqtt.Client, *args, **kwargs):
send_discovery_message(client)
client.subscribe(HA_STATUS_TOPIC, qos=1)


def handle_homeassistant_status(client: mqtt.Client, userdata, message):
status = message.payload.decode()
logger.info(f"Got homeassistant status: {status}")
if status == "online":
send_discovery_message(client)


def get_mqtt_client() -> mqtt.Client:
client = mqtt.Client()
client.on_connect = on_connect
client.message_callback_add(HA_STATUS_TOPIC, handle_homeassistant_status)
if config.MQTT_USERNAME:
client.username_pw_set(config.MQTT_USERNAME, config.MQTT_PASSWORD or None)
return client


def send_co2_value(client: mqtt.Client, value: float):
client.publish(f"{HA_CO2_PREFIX}/state", value, retain=True)


def send_temp_value(client: mqtt.Client, value: float):
client.publish(f"{HA_TEMP_PREFIX}/state", value, retain=True)


def start_client(client: mqtt.Client):
"""Blocking call to connect to the MQTT broker and loop forever"""
logger.info(f"Connecting to {config.MQTT_BROKER}")
client.connect(config.MQTT_BROKER)
client.loop_forever(retry_first_connection=False)
logger.error("MQTT Failure")
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,16 @@ classifiers = [
"Operating System :: OS Independent",
"Development Status :: 3 - Alpha",
]
dependencies = ["prometheus_client"]
dependencies = ["prometheus_client", "environs"]
dynamic = ["version"]

[project.urls]
repository = "https://github.com/jerr0328/co2-mini"

[project.optional-dependencies]
homekit = ["HAP-python"]
homekit = ["hap-python"]
mqtt = ["paho-mqtt"]
all = ["co2mini[homekit,mqtt]"]

[project.scripts]
co2mini = "co2mini.main:main"
Expand Down
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
HAP-Python
environs
hap-python
paho-mqtt
prometheus_client

0 comments on commit 123a13a

Please sign in to comment.