Skip to content

Commit

Permalink
🎉 Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jerr0328 committed Dec 8, 2019
0 parents commit eef5e61
Show file tree
Hide file tree
Showing 9 changed files with 272 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[flake8]
ignore = E203, E266, E501, W503
max-line-length = 80
max-complexity = 18
select = B,C,E,F,W,T4,B9
# We need to configure the mypy.ini because the flake8-mypy's default
# options don't properly override it, so if we don't specify it we get
# half of the config from mypy.ini and half from flake8-mypy.
mypy_config = mypy.ini
131 changes: 131 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

data
6 changes: 6 additions & 0 deletions .isort.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[settings]
multi_line_output=3
include_trailing_comma=True
force_grid_wrap=0
use_parentheses=True
line_length=88
16 changes: 16 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/psf/black
rev: 19.10b0
hooks:
- id: black
- repo: https://github.com/pre-commit/mirrors-isort
rev: v4.3.21
hooks:
- id: isort
5 changes: 5 additions & 0 deletions 90-co2mini.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ACTION=="remove", GOTO="co2mini_end"

SUBSYSTEMS=="usb", KERNEL=="hidraw*", ATTRS{idVendor}=="04d9", ATTRS{idProduct}=="a052", GROUP="plugdev", MODE="0660", SYMLINK+="co2mini%n", GOTO="co2mini_end"

LABEL="co2mini_end"
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# CO2 monitoring with Prometheus

This reads from the CO2 Meter and makes it available as a Prometheus service.
The core logic comes from [this hackaday article](https://hackaday.io/project/5301-reverse-engineering-a-low-cost-usb-co-monitor/log/17909-all-your-base-are-belong-to-us).

## Setup

1. Install Python 3
2. Install python3-prometheus-client
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 `co2_prometheus.service` to `/lib/systemd/system/co2_prometheus.service`
5. Run `systemctl enable co2_prometheus.service`
10 changes: 10 additions & 0 deletions co2_prometheus.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[Unit]
Description=CO2 Monitoring via Prometheus
After=multi-user.target

[Service]
Type=idle
ExecStart=/usr/bin/python3 /home/pi/co2_prometheus/main.py /dev/co2mini0

[Install]
WantedBy=multi-user.target
82 changes: 82 additions & 0 deletions co2_prometheus/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env python3

import fcntl
import logging
import os
import sys

from prometheus_client import Counter, Gauge, start_http_server

co2_gauge = Gauge("co2", "CO2 levels in PPM")
temp_gauge = Gauge("temperature", "Temperature in C")
checksum_error_counter = Counter("checksum_errors", "Number of checksum errors seen")

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

# From http://co2meters.com/Documentation/AppNotes/AN146-RAD-0401-serial-communication.pdf
OP_CO2 = 0x50
OP_TEMP = 0x42


def decrypt(key, data):
# Magic from https://hackaday.io/project/5301-reverse-engineering-a-low-cost-usb-co-monitor/log/17909-all-your-base-are-belong-to-us
cstate = [0x48, 0x74, 0x65, 0x6D, 0x70, 0x39, 0x39, 0x65]
shuffle = [2, 4, 0, 7, 1, 6, 5, 3]

phase1 = [0] * 8
for i, o in enumerate(shuffle):
phase1[o] = data[i]

phase2 = [0] * 8
for i in range(8):
phase2[i] = phase1[i] ^ key[i]

phase3 = [0] * 8
for i in range(8):
phase3[i] = ((phase2[i] >> 3) | (phase2[(i - 1 + 8) % 8] << 5)) & 0xFF

ctmp = [0] * 8
for i in range(8):
ctmp[i] = ((cstate[i] >> 4) | (cstate[i] << 4)) & 0xFF

out = [0] * 8
for i in range(8):
out[i] = (0x100 + phase3[i] - ctmp[i]) & 0xFF

return out


def hd(d):
return " ".join("%02X" % e for e in d)


if __name__ == "__main__":
# Key retrieved from /dev/random, guaranteed to be random ;)
key = [0xC4, 0xC6, 0xC0, 0x92, 0x40, 0x23, 0xDC, 0x96]

fp = open(sys.argv[1], "a+b", 0)

HIDIOCSFEATURE_9 = 0xC0094806
set_report = [0] + key
fcntl.ioctl(fp, HIDIOCSFEATURE_9, bytearray(set_report))

values = {}

# Expose metrics
start_http_server(PROMETHEUS_PORT)

while True:
data = list(fp.read(8))
decrypted = decrypt(key, data)
if decrypted[4] != 0x0D or (sum(decrypted[:3]) & 0xFF) != decrypted[3]:
logger.warning("Checksum error: %s => %s", hd(data), hd(decrypted))
checksum_error_counter.inc()
else:
op = decrypted[0]
val = decrypted[1] << 8 | decrypted[2]

if op == OP_CO2:
co2_gauge.set(val)
if op == OP_TEMP:
temp_gauge.set(val / 16.0 - 273.15)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
prometheus_client

0 comments on commit eef5e61

Please sign in to comment.