A $6 sensor, and knowing what your environment is actually doing
A Raspberry Pi, a DHT22, InfluxDB and Grafana — and what a humidity monitor taught me about writing long-running processes that stay running.
Growing gourmet mushrooms is mostly an exercise in humidity control. Too dry and the fruiting bodies crack and stall. Too wet with no air exchange and you get bacterial blotch, or worse, something green and fuzzy that means you start over.
Lion’s mane is my favorite thing to grow and the least forgiving about it. It wants relative humidity somewhere in the high eighties, and it tells on you when it doesn’t get it — the spines stay stubby and the whole flush yellows at the edges, so instead of a white cascade you get something closer to a tennis ball. It earns the fuss: shredded and pan-fried into crab cakes, it is genuinely difficult to tell from crab.
The cheap hygrometers you can buy for this tell you the humidity right now, on a small LCD, if you are standing in front of them. What they don’t tell you is that it dropped to 68% for four hours overnight while you were asleep, which is the number that actually explains why the crop looks wrong.
I wanted history. So I put a Raspberry Pi in the tent.
The build
The sensor is a DHT22 — about six dollars, reads temperature and relative humidity, one data pin.
It goes on GPIO4, and Adafruit’s adafruit_dht library handles the timing-sensitive single-wire
protocol so you don’t have to.
From there it’s a loop: read the sensor, write two points to InfluxDB, push the values to a 16x2 LCD on the front of the chamber, sleep two seconds, repeat. Grafana sits on top of InfluxDB and draws the graphs.
while True:
temperature_f, humidity = sensor.read_data()
if temperature_f is not None and humidity is not None:
write_api.write(bucket=bucket, org=org,
record=Point("humidity_percent").field("humidity_percent", humidity))
display.print_message(humidity, temperature_f)
time.sleep(2.0)
That’s genuinely most of it. The interesting parts are the two things that loop does not assume.
The DHT22 lies to you, regularly
That if temperature_f is not None is not defensive-programming boilerplate. It’s load-bearing.
The DHT22 fails a read fairly often — checksum mismatches, timing glitches, the Pi getting
preempted mid-transfer. Adafruit’s library raises on these, and the sensor wrapper catches and
returns None. In my experience a few percent of reads come back empty, in bursts.
The naive version of this loop crashes on the first bad read. The slightly-less-naive version
writes None into InfluxDB and you get a graph with holes and a dashboard that renders zero
humidity, which is worse than a hole because it looks like data.
The correct behavior for a sampling loop is: a failed read is not an event. Skip it, don’t record it, don’t log it at warning level, try again in two seconds. You are sampling a physical quantity that changes over minutes; missing one reading costs you nothing. Treating every failed read as an error costs you a log file full of noise and, eventually, the habit of ignoring the logs.
This is the same reasoning that applies to any poller against a flaky dependency, and it’s easier to internalize on a $6 sensor than in production.
A monitor that isn’t running is worse than no monitor
The first version of this ran in a terminal over SSH. It worked perfectly until the power flickered, and then it didn’t run for nine days, and I didn’t notice, because the failure mode of a monitoring tool is silence — which is exactly what it looks like when everything is fine.
The fix is a systemd unit:
[Service]
Type=simple
ExecStart=/usr/bin/python3 /home/pi/pi-humidity-grafana/main.py
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
Restart=always with a short RestartSec, and WantedBy=multi-user.target so it comes back on
boot. Three lines that convert “a script I run” into “a thing that runs.”
I think about this more than the sensor code. A monitoring system that requires you to remember to start it has inverted its own value proposition. If it can’t survive a reboot unattended, it isn’t monitoring — it’s a dashboard you occasionally populate.
The corollary I didn’t implement at the time and should have: alert on absence. A gap in the data is the most important signal the system can produce, and it’s the one nobody builds, because it requires reasoning about what didn’t arrive.
Feature creep, in a good way
Two things got bolted on that weren’t in the original plan.
The 16x2 LCD on the front of the chamber, showing current temp and humidity. Strictly redundant with Grafana. I use it constantly. Walking past and glancing at a number has a completely different cost profile than opening a dashboard, and that difference changes how often you actually look.
A camera stream, running in a daemon thread alongside the sensor loop, serving MJPEG with the current readings overlaid. This turned out to be the more useful half of the project — being able to see the chamber remotely, with the environmental data burned into the frame, answers questions that a graph can’t. It also became the seed of a separate timelapse tool for pulling frames off an MJPEG stream, which I’ve since pointed at completely unrelated things.
The threading here is deliberately unsophisticated — the camera runs as a daemon thread so it dies with the main process, and the two share state through a plain object. For a two-thread program with one writer, that’s sufficient. I’ve seen this kind of project reach for asyncio and end up harder to reason about than the problem warranted.
What it was actually worth
The graphs did solve the original problem: I could finally see the overnight humidity drop, trace it to the misting schedule, and fix it.
But a year on, the thing I actually got out of this was a small, complete, always-on system that I own end to end. It has a sensor, a persistence layer, a visualization layer, a process supervisor, and a failure mode I understand. That’s the same anatomy as any production service, at a scale where you can hold all of it in your head at once.
Everything in my homelab since has been a variation on this: collect something, store it somewhere durable, graph it, and make sure it restarts. The InfluxDB and Grafana instances I stood up for a humidity sensor are still running, and they now have considerably more important things pointed at them.
The lion’s mane is better too.