Skip to content

Commit

Permalink
Define MQTT and sensor configuration separately from implementation
Browse files Browse the repository at this point in the history
The data logger uses a YAML file now, for example like `etc/mois.yaml`.
You will then invoke it like:

  ds18b20-datalogger run etc/mois.yaml
  • Loading branch information
amotl committed Apr 19, 2024
1 parent 63e9a71 commit 61b62a5
Show file tree
Hide file tree
Showing 11 changed files with 300 additions and 214 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
- Tests: Make sensor tests work, using a fake sysfs filesystem
- Tests: Add basic test case for CLI interface
- Remove support for Python 3.7
- Define MQTT and sensor configuration separately from implementation.
The data logger uses a YAML file now, for example like `etc/mois.yaml`.

## v0.0.2 - 2024-04-15
- Publish as `ds18b20-datalogger` package
Expand Down
26 changes: 15 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,15 @@ https://community.hiveeyes.org/t/laborprotokoll-4x5-temp-matrix-mit-ds18b20/5102
when acquired through [Kotori DAQ].


## Synopsis

```shell
ds18b20-datalogger run etc/mois.yaml
```


## Setup

We recommend to install the program into a Python virtualenv.
```shell
python3 -m venv .venv
Expand All @@ -69,22 +77,13 @@ https://community.hiveeyes.org/t/ds18b20-temperatur-sensoren-am-one-wire-bus-ano
ssh youruser@yourpi
screen
source /path/to/ds18b20-datalogger/.venv/bin/activate
ds18b20-datalogger
wget https://github.com/hiveeyes/ds18b20-datalogger/raw/main/etc/mois.yaml
ds18b20-datalogger run mois.yaml
```

### MQTT data upload to Hiveeyes
https://community.hiveeyes.org/t/daten-per-mqtt-und-python-ans-backend-auf-swarm-hiveeyes-org-ubertragen/94/6

### Format your array
https://community.hiveeyes.org/t/how-to-visualize-2-dimensional-temperature-data-in-grafana/974/9
```python
matrix = [[temp_ir_1_1, temp_ir_1_2, temp_ir_1_3, temp_ir_1_4, temp_ir_1_5, temp_ir_1_6],
[temp_ir_2_1, temp_ir_2_2, temp_ir_2_3, temp_ir_2_4, temp_ir_2_5, temp_ir_2_6],
[temp_ir_3_1, temp_ir_3_2, temp_ir_3_3, temp_ir_3_4, temp_ir_3_5, temp_ir_3_6],
[temp_ir_4_1, temp_ir_4_2, temp_ir_4_3, temp_ir_4_4, temp_ir_4_5, temp_ir_4_6],
[temp_ir_5_1, temp_ir_5_2, temp_ir_5_3, temp_ir_5_4, temp_ir_5_5, temp_ir_5_6]]
```

### Data visualization in Grafana
https://swarm.hiveeyes.org/grafana/d/Y9PcgE4Sz/mois-ex-wtf-test-ir-sensor-svg-pixmap-copy

Expand All @@ -96,6 +95,11 @@ https://community.hiveeyes.org/t/temperatursensoren-justieren-kalibrieren/1744/2
*/5 * * * * cd /path/to/data-directory && /path/to/ds18b20-datalogger/.venv/bin/ds18b20-datalogger
```

## Acknowledgements

Ursprünglicher Code zur Datenverarbeitung auf dem Pi:
https://community.element14.com/products/raspberry-pi/raspberrypi_projects/b/blog/posts/multiple-ds18b20-temp-sensors-interfacing-with-raspberry-pi?CommentId=9470e4e9-b054-4dd3-9a3f-ac9d1fe38087


## Contributing

Expand Down
6 changes: 4 additions & 2 deletions docs/backlog.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Backlog for ds18b20-datalogger

## Iteration +1
- Improve documentation
- Publish to PyPI

## Done
- Better software tests
- Break out sensor mapping configuration from code
to make it re-usable across different setups
- Improve documentation
- Publish to PyPI
21 changes: 18 additions & 3 deletions ds18b20_datalogger/cli.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import sys
from pathlib import Path

from ds18b20_datalogger.core import read_ds18b20_sensor_matrix, send_measurement_mqtt
from ds18b20_datalogger.model import Settings


def main():
reading = read_ds18b20_sensor_matrix()
send_measurement_mqtt(reading)
# print(strftime("%Y-%m-%d %H:%M:%S", time.localtime())," Done sending. Going to sleep for 15min.") # noqa: ERA001
if not sys.argv[1:]:
raise ValueError("Program needs a subcommand")
subcommand = sys.argv[1]
if subcommand == "run":
if not sys.argv[2:]:
raise ValueError("Program needs a configuration file")
configfile = Path(sys.argv[2])
if not configfile.exists():
raise ValueError(f"Configuration file does not exist: {configfile}")
settings = Settings.from_file(configfile)
reading = read_ds18b20_sensor_matrix(settings.devicemap)
send_measurement_mqtt(settings.mqtt, reading)

Check warning on line 20 in ds18b20_datalogger/cli.py

View check run for this annotation

Codecov / codecov/patch

ds18b20_datalogger/cli.py#L15-L20

Added lines #L15 - L20 were not covered by tests
else:
raise ValueError(f"Subcommand unknown: {subcommand}")
205 changes: 23 additions & 182 deletions ds18b20_datalogger/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,15 @@
source /path/to/ds18b20-datalogger/.venv/bin/activate
ds18b20-datalogger
```
Ursprünglicher code zur Datenverarbeitung auf dem Pi:
https://community.element14.com/products/raspberry-pi/raspberrypi_projects/b/blog/posts/multiple-ds18b20-temp-sensors-interfacing-with-raspberry-pi?CommentId=9470e4e9-b054-4dd3-9a3f-ac9d1fe38087
""" # noqa: E501

import glob
import json
import os

import paho.mqtt.client as mqtt

from ds18b20_datalogger.model import DeviceMap, MqttSettings, Reading


def read_temp_raw(device_file):
f = open(device_file, "r")
Expand All @@ -43,7 +41,10 @@ def read_temp(device_file):

def try_temp(device_file):
temp_c = -99.0
lines = read_temp_raw(device_file + "/w1_slave")
try:
lines = read_temp_raw(device_file + "/w1_slave")
except FileNotFoundError:
return None
if len(lines) == 2:
if lines[0].strip()[-3:] == "YES":
equals_pos = lines[1].find("t=")
Expand All @@ -52,72 +53,24 @@ def try_temp(device_file):
return temp_c


def send_measurement_mqtt(matrix):
def send_measurement_mqtt(mqtt_settings: MqttSettings, reading: Reading):
"""
Publish measurement to MQTT topic in JSON format.
"""
# The MQTT host
mqtt_host = "swarm.hiveeyes.org"
mqtt_port = 1883
mqtt_user = "username"
mqtt_pass = "some_safe_password" # noqa: S105
# The MQTT topic
mqtt_topic = "{realm}/{network}/{gateway}/{node}/data.json".format(
# Beekeeper collective
realm="hiveeyes",
# Beekeeper-ID
network="testdrive",
# Beehive location
gateway="area42",
# In my case: Not a hive but the gateway.
node="array01",
)
# Define measurement for 30 sensors in 5x6 matrix
measurement = {
"temp-ir-1-1": matrix[0][0],
"temp-ir-1-2": matrix[0][1],
"temp-ir-1-3": matrix[0][2],
"temp-ir-1-4": matrix[0][3],
"temp-ir-1-5": matrix[0][4],
"temp-ir-2-1": matrix[1][0],
"temp-ir-2-2": matrix[1][1],
"temp-ir-2-3": matrix[1][2],
"temp-ir-2-4": matrix[1][3],
"temp-ir-2-5": matrix[1][4],
"temp-ir-3-1": matrix[2][0],
"temp-ir-3-2": matrix[2][1],
"temp-ir-3-3": matrix[2][2],
"temp-ir-3-4": matrix[2][3],
"temp-ir-3-5": matrix[2][4],
"temp-ir-4-1": matrix[3][0],
"temp-ir-4-2": matrix[3][1],
"temp-ir-4-3": matrix[3][2],
"temp-ir-4-4": matrix[3][3],
"temp-ir-4-5": matrix[3][4],
"temp-ir-5-1": matrix[4][0],
"temp-ir-5-2": matrix[4][1],
"temp-ir-5-3": matrix[4][2],
"temp-ir-5-4": matrix[4][3],
"temp-ir-5-5": matrix[4][4],
"temp-ir-6-1": matrix[5][0],
"temp-ir-6-2": matrix[5][1],
"temp-ir-6-3": matrix[5][2],
"temp-ir-6-4": matrix[5][3],
"temp-ir-6-5": matrix[5][4],
}
# Serialize data as JSON
payload = json.dumps(measurement)
# Publish to MQTT
# Serialize reading to JSON.
payload = json.dumps(reading.to_dict())
# Publish to MQTT.
pid = os.getpid()
client_id = "{}:{}".format("mois-temp-matrix", str(pid))
client_id = "{}:{}".format(mqtt_settings.client_id, str(pid))
backend = mqtt.Client(client_id=client_id, clean_session=True)
backend.username_pw_set(mqtt_user, mqtt_pass)
backend.connect(mqtt_host, mqtt_port)
backend.publish(mqtt_topic, payload)
if mqtt_settings.username and mqtt_settings.password:
backend.username_pw_set(mqtt_settings.username, mqtt_settings.password)
backend.connect(mqtt_settings.host, mqtt_settings.port or 1883)
backend.publish(mqtt_settings.topic, payload)
backend.disconnect()


def read_ds18b20_sensor_matrix():
def read_ds18b20_sensor_matrix(devicemap: DeviceMap) -> Reading:
"""
Acquire measurement reading from an array matrix of DS18B20 sensors, connected to a Raspberry Pi machine.
Expand All @@ -127,125 +80,13 @@ def read_ds18b20_sensor_matrix():
ls -la /sys/bus/w1/devices/
"""

temp_ir_1_1 = temp_ir_1_2 = temp_ir_1_3 = temp_ir_1_4 = temp_ir_1_5 = None
temp_ir_2_1 = temp_ir_2_2 = temp_ir_2_3 = temp_ir_2_4 = temp_ir_2_5 = None
temp_ir_3_1 = temp_ir_3_2 = temp_ir_3_3 = temp_ir_3_4 = temp_ir_3_5 = None
temp_ir_4_1 = temp_ir_4_2 = temp_ir_4_3 = temp_ir_4_4 = temp_ir_4_5 = None
temp_ir_5_1 = temp_ir_5_2 = temp_ir_5_3 = temp_ir_5_4 = temp_ir_5_5 = None
temp_ir_6_1 = temp_ir_6_2 = temp_ir_6_3 = temp_ir_6_4 = temp_ir_6_5 = None

# Mount the device.
# Install drivers, in order to provide sysfs-based
# access to devices connected to the one-wire bus.
os.system("modprobe w1-gpio") # noqa: S605, S607
os.system("modprobe w1-therm") # noqa: S605, S607

# Get all the filenames begin with 28 in the path base_dir.
base_dir = "/sys/bus/w1/devices/"
device_folders = glob.glob(base_dir + "28*")

# print device_folders # DEBUG

for folder in device_folders:
tc = read_temp(folder)
label = "no_known_label"
if folder == "/sys/bus/w1/devices/28-0346d4430b06":
label = "temp-ir-1-1"
temp_ir_1_1 = tc
if folder == "/sys/bus/w1/devices/28-0cf3d443ba40":
label = "temp-ir-1-2"
temp_ir_1_2 = tc
if folder == "/sys/bus/w1/devices/28-0e49d44343bd":
label = "temp-ir-1-3"
temp_ir_1_3 = tc
if folder == "/sys/bus/w1/devices/28-11c1d443f241":
label = "temp-ir-1-4"
temp_ir_1_4 = tc
if folder == "/sys/bus/w1/devices/28-1937d443bed6":
label = "temp-ir-1-5"
temp_ir_1_5 = tc
if folder == "/sys/bus/w1/devices/28-2231d443d266":
label = "temp-ir-2-1"
temp_ir_2_1 = tc
if folder == "/sys/bus/w1/devices/28-282bd4430f5e":
label = "temp-ir-2-2"
temp_ir_2_2 = tc
if folder == "/sys/bus/w1/devices/28-2846d443e4f2":
label = "temp-ir-2-3"
temp_ir_2_3 = tc
if folder == "/sys/bus/w1/devices/28-297ad443f622":
label = "temp-ir-2-4"
temp_ir_2_4 = tc
if folder == "/sys/bus/w1/devices/28-2c6cd443ccf7":
label = "temp-ir-2-5"
temp_ir_2_5 = tc
if folder == "/sys/bus/w1/devices/28-304ad4436d1a":
label = "temp-ir-3-1"
temp_ir_3_1 = tc
if folder == "/sys/bus/w1/devices/28-32c9d443b51b":
label = "temp-ir-3-2"
temp_ir_3_2 = tc
if folder == "/sys/bus/w1/devices/28-3ce1d443d148":
label = "temp-ir-3-3"
temp_ir_3_3 = tc
if folder == "/sys/bus/w1/devices/28-400bd4439156":
label = "temp-ir-3-4"
temp_ir_3_4 = tc
if folder == "/sys/bus/w1/devices/28-450ed4430afe":
label = "temp-ir-3-5"
temp_ir_3_5 = tc
if folder == "/sys/bus/w1/devices/28-4694d44325ca":
label = "temp-ir-4-1"
temp_ir_4_1 = tc
if folder == "/sys/bus/w1/devices/28-5196d4434d53":
label = "temp-ir-4-2"
temp_ir_4_2 = tc
if folder == "/sys/bus/w1/devices/28-550cd4434596":
label = "temp-ir-4-3"
temp_ir_4_3 = tc
if folder == "/sys/bus/w1/devices/28-57b6d44367cb":
label = "temp-ir-4-4"
temp_ir_4_4 = tc
if folder == "/sys/bus/w1/devices/28-5821d44339c2":
label = "temp-ir-4-5"
temp_ir_4_5 = tc
if folder == "/sys/bus/w1/devices/28-65f2d4434f51":
label = "temp-ir-5-1"
temp_ir_5_1 = tc
if folder == "/sys/bus/w1/devices/28-6723d443b9ea":
label = "temp-ir-5-2"
temp_ir_5_2 = tc
if folder == "/sys/bus/w1/devices/28-6755d443de81":
label = "temp-ir-5-3"
temp_ir_5_3 = tc
if folder == "/sys/bus/w1/devices/28-6950d443323f":
label = "temp-ir-5-4"
temp_ir_5_4 = tc
if folder == "/sys/bus/w1/devices/28-6ae6d4434899":
label = "temp-ir-5-5"
temp_ir_5_5 = tc
if folder == "/sys/bus/w1/devices/28-6b2ad4437e19":
label = "temp-ir-6-1"
temp_ir_6_1 = tc
if folder == "/sys/bus/w1/devices/28-6f8ad443a557":
label = "temp-ir-6-2"
temp_ir_6_2 = tc
if folder == "/sys/bus/w1/devices/28-72d1d44362f0":
label = "temp-ir-6-3"
temp_ir_6_3 = tc
if folder == "/sys/bus/w1/devices/28-72d8d44397f7":
label = "temp-ir-6-4"
temp_ir_6_4 = tc
if folder == "/sys/bus/w1/devices/28-79d2d4430cf1":
label = "temp-ir-6-5" # noqa: F841
temp_ir_6_5 = tc
# print ('%s %3.1f deg C %s' % (folder, tc, label)) # noqa: ERA001
# print ('%s %3.1f' % (label, tc)) # noqa: ERA001
matrix = [
[temp_ir_1_1, temp_ir_1_2, temp_ir_1_3, temp_ir_1_4, temp_ir_1_5],
[temp_ir_2_1, temp_ir_2_2, temp_ir_2_3, temp_ir_2_4, temp_ir_2_5],
[temp_ir_3_1, temp_ir_3_2, temp_ir_3_3, temp_ir_3_4, temp_ir_3_5],
[temp_ir_4_1, temp_ir_4_2, temp_ir_4_3, temp_ir_4_4, temp_ir_4_5],
[temp_ir_5_1, temp_ir_5_2, temp_ir_5_3, temp_ir_5_4, temp_ir_5_5],
[temp_ir_6_1, temp_ir_6_2, temp_ir_6_3, temp_ir_6_4, temp_ir_6_5],
]
# print (matrix) # noqa: ERA001
return matrix
reading = Reading()
for device in devicemap.devices:
value = read_temp(device.path)
reading.add_measurement(name=device.name, value=value)
return reading
Loading

0 comments on commit 61b62a5

Please sign in to comment.