DEV Community

Aquarium Pet UK
Aquarium Pet UK

Posted on

Logging Aquarium Water Temperature with a Raspberry Pi, a DS18B20 and ~60 Lines of Python

Disclosure: I'm affiliated with aquariumpet.co.uk, a UK shop that sells aquarium heaters.

An aquarium heater's display tells you what the water is doing right now, near the rod. It doesn't tell you what happened at 3 a.m., whether the far end of the tank lags behind, or how long the heater actually runs each day. For that you need a log. This tutorial builds a small, independent temperature logger with a Raspberry Pi and a DS18B20 sensor. It writes a CSV and warns you when the temperature leaves a band you choose. Then we'll use the log to estimate how much electricity the heater uses.

What you need

  • Any Raspberry Pi with GPIO pins (a Zero 2 W is fine)
  • A waterproof DS18B20 probe (the stainless-steel, cabled version)
  • One 4.7 kΩ resistor (pull-up between DATA and 3.3 V)
  • Python 3 (already on Raspberry Pi OS)

1. Wire it and enable 1-Wire

The DS18B20 has three wires: VDD to 3.3 V (pin 1), GND to ground (pin 6), and DATA to GPIO4 (pin 7), with the 4.7 kΩ resistor across DATA and 3.3 V.

Turn on the 1-Wire bus:

sudo raspi-config   # Interface Options -> 1-Wire -> Enable
sudo reboot
ls /sys/bus/w1/devices/   # you should see a folder starting with 28-
Enter fullscreen mode Exit fullscreen mode

Each sensor shows up as a folder, and its w1_slave file holds the latest reading in thousandths of a degree.

Put the probe somewhere away from the heater, ideally at the opposite end of the tank, so you see what the fish see and not the warm water right by the rod.

2. The logger

#!/usr/bin/env python3
"""Minimal aquarium temperature logger for DS18B20 on a Raspberry Pi."""
import csv, glob, time
from datetime import datetime
from pathlib import Path

LOG_FILE = Path.home() / "tank_temp.csv"
INTERVAL_S = 60          # one reading per minute
LOW_C, HIGH_C = 24.0, 27.0   # your own target band; set to suit your livestock

def sensor_path() -> str:
    devices = glob.glob("/sys/bus/w1/devices/28-*/w1_slave")
    if not devices:
        raise RuntimeError("No DS18B20 found - check wiring and 1-Wire setting")
    return devices[0]

def read_c(path: str) -> float | None:
    for _ in range(3):                      # retry on CRC failure
        lines = Path(path).read_text().splitlines()
        if lines[0].strip().endswith("YES"):
            return int(lines[1].split("t=")[1]) / 1000.0
        time.sleep(0.5)
    return None

def main() -> None:
    path = sensor_path()
    new = not LOG_FILE.exists()
    with LOG_FILE.open("a", newline="") as f:
        w = csv.writer(f)
        if new:
            w.writerow(["timestamp", "temp_c"])
        while True:
            t = read_c(path)
            now = datetime.now().isoformat(timespec="seconds")
            w.writerow([now, "" if t is None else f"{t:.3f}"])
            f.flush()
            if t is None:
                print(f"{now} sensor read failed")
            elif not LOW_C <= t <= HIGH_C:
                print(f"{now} OUT OF RANGE: {t:.2f} °C")
            time.sleep(INTERVAL_S)

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Notes:

  • The first line of w1_slave ends in YES when the CRC checks out. If it doesn't, we retry instead of logging nonsense.
  • Failed reads go into the log as blank cells, so gaps in the data are obvious and nothing gets silently filled in.
  • Swap the print calls for a webhook, email or ntfy push if you want alerts on your phone.

Run it as a service so it survives reboots:

# /etc/systemd/system/tanklog.service
[Unit]
Description=Aquarium temperature logger
After=multi-user.target

[Service]
ExecStart=/usr/bin/python3 /home/pi/tanklog.py
Restart=always
User=pi

[Install]
WantedBy=multi-user.target
Enter fullscreen mode Exit fullscreen mode
sudo systemctl enable --now tanklog
Enter fullscreen mode Exit fullscreen mode

3. Reading the log: daily min/max and swing

After a day or two, a few lines of pandas give you the useful numbers:

import pandas as pd

df = pd.read_csv("~/tank_temp.csv", parse_dates=["timestamp"]).dropna()
daily = df.set_index("timestamp")["temp_c"].resample("D").agg(["min", "max", "mean"])
daily["swing"] = daily["max"] - daily["min"]
print(daily.round(2))
Enter fullscreen mode Exit fullscreen mode

A large daily swing usually means one of three things: the heater is undersized, it's sitting in poor water flow, or the room gets very cold overnight. A log can't tell you which, but it does show you there's a problem.

4. Estimating heater energy from the log

Most heaters are thermostatic: they draw roughly their rated power while heating and close to nothing while holding. If you know the share of the time the heater is on (its duty cycle), the energy maths is easy:

def daily_kwh(rated_watts: float, duty_cycle: float) -> float:
    return rated_watts * duty_cycle * 24 / 1000

def daily_cost(rated_watts: float, duty_cycle: float, pence_per_kwh: float) -> float:
    return daily_kwh(rated_watts, duty_cycle) * pence_per_kwh / 100  # pounds
Enter fullscreen mode Exit fullscreen mode

The temperature probe can't measure duty cycle directly. Here are two simple ways to get it:

  1. Smart plug with energy monitoring. This is the most accurate option. Read the kWh figure straight off it and skip the guesswork.
  2. Infer it from the log. On a heater with its own thermostat, stretches where the temperature is rising steadily are roughly "on" and stretches where it's falling are "off." Count the rising minutes over a day and divide by 1,440. It's crude, but it's enough to compare one week with the next or see the effect of putting a lid on the tank.

Plug in your own tariff (it's on your energy bill) instead of using someone else's number.

Where the heater fits in

The logger gives you an independent check on whatever heater you run. If you're choosing one, make sure the manufacturer's tank-size rating matches your tank and that the adjustable range covers your target. For example, the XINDI XD-L880 500W quartz heater is listed for tanks up to 300 L (about 100 cm) with a 20–34 °C setting range. Its set button and display are on the cable rather than on the rod, so you can adjust it without putting your hands in the water. The listing also explains its display codes: LL flashes below 20 °C, HH flashes above 34 °C (heating stops), and EE means a sensor fault (heating stops). Those are worth knowing when your own log shows something unexpected.

Whatever you use, the install basics on that listing apply to most heaters: keep the rod fully submerged and upright in a flow of water, and don't bury it in the gravel. Placement matters as much as wattage, and the logger is how you check it.

Wrap-up

For the cost of a sensor and a resistor you get a continuous, independent record of your tank's temperature, alerts when it drifts, and real data for energy estimates. Next steps could be a second probe at the other end of the tank, a Grafana dashboard, or pushing readings to MQTT.

To be clear about my relationship: I'm affiliated with aquariumpet.co.uk, which sells the heater linked above.

Top comments (0)