Monitoring my freezer with a Raspberry Pi, Grafana/Prometheus, and a bit of Golang.
Motivation — the day the ice cream melted
One day my wife calls me downstairs and says “The ice cream in the freezer is melted”. That’s a bad sign. That means the 10 year old freezer in the garage has an issue. I’m handy, but nope I couldn’t fix it. Spending $600 on a new freezer is bad enough, but having to spend a bunch of money to replace spoiled food is adding insult to injury. Better to catch the failure before the food thaws.
So I’m faced with a choice — I can buy a commercial product, a decent one by Thermoworks, for around a hundred bucks. It would use their closed-source cloud service. I could buy a no-name alternative on Amazon for around $40 bucks, also using someone’s proprietary closed-source cloud service. Alternatively, I could just spend $15 on a raspberry pi zero 2 w, $2 on a temperature sensor, and monitor it myself. The benefits of doing it myself:
- Nobody’s closed-source no-api opaque cloud service is required. My data and my access are my own.
- Fine granularity. I can log the temperature every fifteen seconds.
- I can store the readings forever.
- I can coordinate it with energy usage.
- I can write intelligent alerts that fire after the temperature has been high for a specific amount of time. Reduce false alarms. Distinguish between freezer failure and the door being left open for a short period.
- I can send alerts via email or other methods.
- Future: Could I pair this with anomaly detection to detect the next freezer failure before it happens?
Hardware
My hardware of choice for this type of task is the Raspberry Pi zero 2 w. This is a small power-efficient board, using much less energy than the Raspberry Pi 5 or similar full-size boards. There are lots of alternatives that could be chosen, such as small esp8266 devices, Raspberry Pi Pico, etc. I chose the zero 2 w because it is a full-fledged linux computer that is easy to develop on, that I’m very familiar with. It’s nice being able to SSH into your temperature monitor if you want to. The Raspberry Pi website will tell you where to buy one. Around 15 bucks, a little more if you need a case or a power supply or want the GPIO header pre-soldered for you.
The next bit of hardware we need is the temperature sensor itself. I chose to use a DS18B20. You can find these cheap on amazon or ebay, in a variant that is encapsulated and waterproof. A freezer is a zone of shifting temperatures and humidities — it’s good to have an encapsulated device. I bought a 5-pack on amazon from a vendor named hitlego:
You’ll also need a 4.7K resistor. You can get a pack of a hundred of them from Amazon for about 5 bucks.
When wiring things up, connect as follows:
- +V (red wire) from Sensor to Raspberry Pi 3.3V
- GND (black wire) from Sensor to Raspberry Pi GND.
- Data (yellow wire) from Sensor to Raspberry Pi GPIO4.
- Resistor between 3.3V and GPIO4.
It also helps to put the pi in a case. I 3D printed a nice small case that included some embedded magnets so it sticks nicely to the freezer’s cabinet.
Here’s a picture of the case and various parts:
… and here’s what it looks like after the case is assembled:
If you’re wondering what the additional red/black cable is, this is a round “barrel” power jack. I’m not a fan of micro-USB connectors for powering my projects, so I often add in a traditional power jack.
The small green thing is a 3D printed clip to hold the probe. It’s printed in TPU, a flexible material.
Software — Collecting temperatures and exporting to Prometheus
Usually I use python for my Raspberry Pi projects, but I’m all about Golang these days, so I wrote a Go program. The code is in my github repository.
Our first job is to read the sensor data. The Raspberry Pi’s Linux, Raspbian, already comes with a driver for the 1-wire protocol. The INSTALL.md file in my github repo explains how to enable it. This is almost a pity, as it would have been fun to bit-bang GPIO using golang on the pi. Anyhow, this turns getting the sensor data into an exercise in opening files in the /sys filesystem:
func (ds *DS18B20) MeasureDevice(name string) (float64, error) {
deviceFileName := fmt.Sprintf("%s/%s/w1_slave", DEVICE_DIR, name)
if _, err := os.Stat(deviceFileName); os.IsNotExist(err) {
return 0.0, fmt.Errorf("device file %s does not exist", deviceFileName)
}
fileContents, err := os.ReadFile(deviceFileName)
if err != nil {
return 0.0, fmt.Errorf("error reading device file %s: %v", deviceFileName, err)
}
lines := strings.Split(string(fileContents), "\n")
if len(lines) < 2 {
return 0.0, fmt.Errorf("unexpected format in device file %s", deviceFileName)
}
if !strings.HasSuffix(lines[0], "YES") {
return 0.0, fmt.Errorf("first line does not end with yes in %s: %s", deviceFileName, lines[0])
}
parts := strings.Split(lines[1], "t=")
if len(parts) != 2 {
return 0.0, fmt.Errorf("unexpected format in second line of %s: %s", deviceFileName, lines[1])
}
tempC, err := strconv.ParseFloat(parts[1], 64)
if err != nil {
return 0.0, fmt.Errorf("error parsing temperature in %s: %v", deviceFileName, err)
}
return tempC / 1000.0, nil
}
It’s not my most elegant code — it was a hasty port from a hastily written python source from a decade or so ago. Our next task is to create a prometheus endpoint so the data can be scraped:
// import "github.com/prometheus/client_golang/prometheus"
// import "github.com/prometheus/client_golang/prometheus/promhttp"
// Create the prometheus gauge
temperature := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "temperature_celsius",
Help: "Current temperature in Celsius",
})
up := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "up",
Help: "Whether the collector is healthy (1 = healthy, 0 = failed)",
})
registry := prometheus.NewRegistry()
registry.MustRegister(temperature)
registry.MustRegister(up)
// Create a goroutine to periodically measure the temperature
go func() {
// if for any reason the goroutine exits, make sure we set the up gauge to 0
defer func() {
fmt.Fprintf(os.Stderr, "Collection goroutine as exited, setting Up to 0\n")
up.Set(0)
}()
for {
tempC, err := ds.MeasureFirstDevice()
if err != nil {
fmt.Fprintf(os.Stderr, "Error measuring temperature: %v\n", err)
temperature.Set(0) // Set to 0 on error
up.Set(0)
} else {
if verbose {
log.Printf("Sampled Temperature: %.2f°C", tempC)
}
temperature.Set(tempC)
up.Set(1)
}
time.Sleep(10 * time.Second)
}
}()
// Add the metrics handler to the http server
http.Handle(
"/metrics", promhttp.HandlerFor(
registry,
promhttp.HandlerOpts{
EnableOpenMetrics: true,
}),
)
// Start the HTTP server
err = http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Fprintf(os.Stderr, "Error starting HTTP server: %v\n", err)
os.Exit(-1)
}
You’ll notice I create two metrics. One of them is temperature_celsius to report the current temperature. The other is up to report whether or not the temperature collection is working. The up metric is important because we want to throw an alarm not only if the freezer is warm, but also if samples are not being collected.
Finally, I want to be able to daemonize this so it runs properly as a background process. Fortunately, Sergey Yarmonov created an excellent library called go-daemon that can be used for this:
func daemonize() {
cntxt := &daemon.Context{
PidFileName: PIDFILE,
PidFilePerm: 0644,
LogFileName: LOGFILE,
LogFilePerm: 0640,
WorkDir: "./",
Umask: 027,
}
d, err := cntxt.Reborn()
if err != nil {
log.Fatal("Unable to run: ", err)
}
if d != nil {
return
}
defer cntxt.Release() // nolint:errcheck
log.Print("- - - - - - - - - - - - - - -")
log.Print("daemon started")
run()
}
If you want to implement a similar temperature monitor, then I encourage you to download the github repo as I have left a bit out here in this post.
Configuring Prometheus
I use prometheus to scrape the metrics. Note that my version of prometheus is ancient — this is a home project, and I have not updated prometheus in … a while. So there might be some configuration syntax issues if the config files have changed.
prometheus.yml:
scrape_configs:
# freezer monitor
- job_name: 'freezermon'
metrics_path: /metrics
static_configs:
- targets:
- <my-ip-address-here>:8080
we also need to setup alerting:
alerts:
groups:
- name: freezer-monitoring
rules:
- alert: ScrapeDown
expr: up{instance="<my-ip-address-here>:8080",job="freezermon"}!=1
for: 5m
labels:
severity: major
annotations:
summary: "garage freezer monitor is down"
description: "garage freezer monitor is down"
- alert: FreezerWarm
expr: temperature_celsius{instance="<my-ip-address-here>:8080",job="freezermon"}>-3
for: 20m
labels:
severity: major
annotations:
summary: "garage freezer is warm"
description: "garage freezer monitor is warm"
Here I have two alarms configured. The first will alert me if scraping is down. The second will alert me if the freezer is warm for more than 20 minutes. The time duration is important as when you open the freezer door it actually takes it a while to cool back down. To minimize false alarms, it takes some trial and error to set the duration.
Note that the temperature threshold “-3” is in celsius because those are the units my prometheus exporter uses. It’s important to test this — open the freezer, touch the sensor for a few minutes (this will get it good and warm), and then make sure the alarm fires and the email is sent.
Visualizing it in Grafana
I’m not going to post the very verbose json grafana dashboard source here. It’s easy enough to create a dashboard in the Grafana UI and connect it to the metrics. Here is what mine currently looks like:
There are two graphs present. The green one is the freezer monitor. The blue one is my home energy monitor connected to the garage circuit (which includes not only the freezer, but other items such as the garage lights). Here’s a close-up picture:
The two large spikes in the green graph are when I opened the freezer door to retrieve items. After opening the door, it takes a couple of hours for the probe to come back down to minimum temperature.
The many smaller oscillations are the normal cooling cycle of the freezer. This is not an inverter-driven variable speed compressor. It’s a simple on/off cycle compressor, which means it has a threshold where it turns the compressor on when it’s too hot and then shuts it back off when it’s too cold.
One thing that surprised me was that the duty cycle is so high. The freezer is running more than 50% of the time. I did buy the cheapest freezer that the store sold. It still cost me a solid $600 plus delivery though. The freezer consumes about 80 watts when it’s running. Back of the envelope calculations at around 50% duty cycle would turn that into somewhere around a kilowatt-hour per day, or 30 kilowatt-hours per month, or 360 kwH per year. The energy guide from the freezer says 466 kWh per year, so my back-of-the-envelope calculations are at least in the ballpark.
Questions and Answers
First of all, thank you Hackaday readers for all of the useful comments. I’m addressing some of them here.
- [Appropriateness of GPT-generated image] You were right, pictures of the actual hardware makes a better top-of-blog image than the GPT image by itself.
- [Raspberry Pi is overkill] I did consider lighter solutions than using a Pi Zero 2 W. Ultimately it came down to what I had on hand and what I was prepared to implement in a short amout of time. I do have some Picos, but not the Wifi variety. I do have some ESP8266, but my development cycle with them is longer than my development cycle with the Pi, especially when we start adding things like prometheus exporters. Using prometheus and grafana I considered to be a required feature, as I use it to monitor many other things around the house.
- [How the Sensor Wire enters the freezer] The sensor wire enters along the front of the freezer, where it is pressed on by the door seal. I don’t “feel” any temperature there and the seal is a double-lip design so I’m not particularly worried about cooling leakage. I did also compare energy utilization between the old (without the monitor) and new freezer (with the monitor) and see no significant difference. I probably will put the thermal camera on it at some point and have a look. If it’s a problem, I can add weather stripping. I’m hesitant to punch a hole in the freezer cabinet, as it is a brand new freezer with a warranty. The commenters on Hackaday had many good suggestions, including stripping the insulation off the cable and flattening it, using a flex PCB, using a flat ribbon cable, etc.
- [Location of Temperature Sensor] It’s located about a foot deep in the freezer along the top right corner. Maybe locating it further in, or at one of the shelves would have led to a more stable temperature. I’ll have to experiment. I was somewhat constrained by the length of the sensor cable, which I had opted to not extend (it seemed plenty long at the time).
- [Using Batteries] My choice to use a Pi mandated the need for a DC power source rather than a battery-powered solution. This is one of the advantages of choosing a more lightweight solution, such as an eps8266 and going into a low-power mode between measurements. If you ran off batteries, you could conceivably locate the device entirely inside the freezer, though potentially with compromised Wifi signal, and with the disadvantage of batteries inside your freezer. It is an interesting idea though.
- [Using Home Assistant] I know many people use Home Assistant for Home Automation tasks, but I’m not one of them. I don’t have anything against the software, but I’ve been using Prometheus and Grafana for many of my monitoring projects.
- [Ice cream would never survive long enough to melt] Any lost ice cream, no matter how small the quantity, is considered a disaster around here. I’m not even sure one raspberry pi is good enough to cover it.
Next steps
If you found this article interesting
- Reply here on Medium to let me know if you have any suggestions, alternatives, etc.
- Connect with Scott on LinkedIn if you want to learn more about me.
(credits: The image at the top of the blog was created using GPT-4o on Poe using the prompt “picture of an upright freezer connected to a computer for temperature monitoring, together with a graph and an alarm siren. Suitable for a professional blog. Be humorous and use a vintage theme.”)