Smart Coffee Machine Pump Controlled by Raspberry Pi & HC-SR04 Ultrasonic Sensor

Alex Stakhanov
Cloud4RPi
Published in
8 min readNov 12, 2019

In theory, every time you go to the coffee machine for your morning cup, there’s only a one-in-twenty chance you’ll have to fill the water tank. In practice, however, it seems that the machine somehow finds a way to always put this chore on you. The more you want coffee, the more likely you are to get the dreaded “fill the water tank” message. My colleagues feel the same way about this. Being the nerds that we are, we decided to implement the technology that would put an end to this.

Our Equipment

We have a SAECO Aulika Focus coffee machine. Up to this day, we used a hand pump to fill the machine’s water tank from a standard 5 Gallon (19L) water bottle.

Our Goals

  1. Use an electric pump driven by some kind of a controller or a microcomputer through a relay.
  2. Have a way to measure the water level in the coffee machine’s tank so our system knows when to refill it.
  3. Have means to control the system, preferably in real-time from a mobile device.
  4. Receive notifications (through Slack or a similar service) if anything goes wrong with the system.

1. Choosing the Equipment

The Pump

A quick web search will show several electric pump models designed for your water bottle of choice. Such pumps are usually controlled by an ON/OFF switch (for example, Hot Frost A12 or SMixx ХL-D2). Here’s the pump we chose for our project:

Hot Frost A12 Electric Water Pump
Hot Frost A12 Electric Water Pump

The Controller Device

We tried several devices but settled on a Raspberry Pi due to the following advantages:

  • It has a GPIO that allows us to connect a proximity sensor
  • It supports Python

We installed a fresh version of Raspbian Buster Lite and everything required to run Python 3.

How We Toggle the Pump

To control the power, we picked a medium power (12V/2A) solid state relay suited for alternating current.

Relay Powering the Pump
Relay Powering the Pump

The relay connects the pump to the outlet and is controlled by the Raspberry Pi’s digital pin.

How We Check the Water Level

It was important for us to not alter the coffee machine’s construction, so we decided to use the HC-SR04 Ultrasonic proximity sensor to measure the water level.

Raspberry Pi & Ultrasonic Distance Measurement Module HC-SR04
Raspberry Pi & Ultrasonic Distance Measurement Module HC-SR04

We 3d-printed a custom water tank cover with two holes for the sensor’s emitters.

3D-Printed Custom Water Tank Cover for an HC-SR04 Sensor
3D-Printed Custom Water Tank Cover for an HC-SR04 Sensor

We easily found a GitHub library for the sensor.

At this point all preparations were finished.

2. Designing and Running the System

System’s Logic

The system is designed with the following simple logic in mind:

  • The system constantly monitors the distance between the sensor and the water surface.
  • Whenever a change in distance goes over a threshold value, the system sends information about its state to the cloud.
  • If the distance goes over the maximum allowed value (the tank is empty), the system activates the pump and turns it off once the distance is less than the minimum allowed value.
  • Whenever the system’s state changes (for example, the pump activates), it informs the cloud.
Smart Water Pump System Diagram
Smart Water Pump System Diagram

In case of an error, a notification is sent to a Slack channel.

Notification to the Slack channel
Notification to the Slack channel

When the coffee machine is idle, the system pings the cloud service with diagnostic data once every minute. Additionally, it sends its state to the cloud every 5 minutes.

When the pump is active, the system sends data more frequently but no more than once every half a second.

def send(cloud, variables, dist, error_code=0, force=False):
pump_on = is_pump_on()
percent = calc_water_level_percent(dist)
variables['Distance']['value'] = dist
variables['WaterLevel']['value'] = percent
variables['PumpRelay']['value'] = pump_on
variables['Status']['value'] = calc_status(error_code, percent, pump_on)

current = time()
global last_sending_time
if force or current - last_sending_time > MIN_SEND_INTERVAL:
readings = cloud.read_data()
cloud.publish_data(readings)
last_sending_time = current

Working with the Pump

We define the following constants as a base for pump operation logic.

# GPIO Pins (BCM)
GPIO_PUMP = 4
GPIO_TRIGGER = 17
GPIO_ECHO = 27
# Pump
START_PUMP = 1
STOP_PUMP = 0
PUMP_BOUNCE_TIME = 50 # milliseconds
PUMP_STOP_TIMEOUT = 5 # secs

IMPORTANT: If you are going to use Pin 4, do not forget to disable the 1-Wire raspi-config option to avoid conflicts.

At the program’s startup, we register a callback and set the initial state to OFF.

GPIO.setmode(GPIO.BCM)
GPIO.setup(GPIO_PUMP, GPIO.IN)
GPIO.add_event_detect(GPIO_PUMP, GPIO.BOTH, callback=pump_relay_handle, bouncetime=PUMP_BOUNCE_TIME)toggle_pump(STOP_PUMP)

Here’s the code for the function that toggles the pump:

def toggle_pump(value):
if pump_disabled:
return
if is_pump_on() != value:
log_debug("[x] %s" % ('START' if value else 'STOP'))
GPIO.setup(GPIO_PUMP, GPIO.OUT)
GPIO.output(GPIO_PUMP, value) # Start/Stop pouring

As defined in the startup code above, when the relay turns ON, the following callback is called:

pump_on = Falsedef pump_relay_handle(pin):
global pump_on
pump_on = GPIO.input(GPIO_PUMP)
log_debug("Pump relay changed to %d" % pump_on)

In the callback, we save the pump’s current state to a variable.

In the application’s main loop, we can detect the moment when the pump toggles as shown below:

def is_pump_on():
global pump_on
return pump_on
if GPIO.event_detected(GPIO_PUMP):
is_pouring = is_pump_on()
# ...
log_debug('[!] Pump event detected: %s' % ('On' if is_pouring else 'Off'))
send(cloud, variables, distance, force=True)

Measuring the Distance

It’s quite easy to measure the distance towards the water surface using an ultrasonic proximity sensor. In our repository, we shared a couple of python scripts that allow you to test a sensor.

In real applications, sensor readings can fluctuate because of the sensor’s bouncing effect and water oscillations. In some cases, readings can be completely missing.

We implemented a BounceFilter class that accumulates N recent values, discards peaks and calculates the average of remaining measurements.

The measurement process is implemented by the following asynchronous algorithm.

# Keeps the last sensor measurements
readings = BounceFilter(size=6, discard_count=1)

reading_complete = threading.Event()

def wait_for_distance():
reading_complete.clear()
thread = threading.Thread(target=read_distance)
thread.start()

if not reading_complete.wait(MAX_READING_TIMEOUT):
log_info('Reading sensor timeout')
return None
return readings.avg()


def read_distance():
try:
value = hcsr04.raw_distance(sample_size=5)
rounded = value if value is None else round(value, 1)
readings.add(rounded)
except Exception as err:
log_error('Internal error: %s' % err)
finally:
reading_complete.set()

You can find the filter’s full implementation in the sources.

Handling Emergency Situations

What if the sensor burned out, or fell off, or points to a wrong area? We needed a way to report such cases so that we can take manual action.

If the sensor fails to provide distance readings, the system sends the changed status to the cloud and generates a corresponding notification.

Cloud4RPi Sensor Status Widget
Cloud4RPi Sensor Status Widget

The logic is illustrated by the code below.

distance = wait_for_distance() # Read the current water depthif distance is None:
log_error('Distance error!')
notify_in_background(calc_alert(SENSOR_ERROR))
send(cloud, variables, distance, error_code=SENSOR_ERROR, force=True)

We have an operational water level range that should be maintained when the sensor is in its place. We test if the current water level falls in this range:

# Distance from the sensor to the water level 
# based on the coffee-machine's water tank
MIN_DISTANCE = 2 # cm
MAX_DISTANCE = 8 # cm
# Distance is out of expected range: do not start pouring
if distance > MAX_DISTANCE * 2:
log_error('Distance is out of range: %.2f' % distance)
continue

We turn the pump off if it was active when an error occurred.

if is_pump_on() and prev_distance < STOP_PUMP_DISTANCE + DISTANCE_DELTA:
log_error('[!] Emergency stop of the pump. No signal from a distance sensor')
toggle_pump(STOP_PUMP)

We also process the case when the bottle runs out of water. We check if the water level does not change when the pump runs. If so, the system waits for 5 seconds and then checks if the pump has turned off. If it has not, then the system implements emergency pump shutdown and sends an error notification.

PUMP_STOP_TIMEOUT = 5   # secs
emergency_stop_time = None
def set_emergency_stop_time(now, is_pouring):
global emergency_stop_time
emergency_stop_time = now + PUMP_STOP_TIMEOUT if \
is_pouring else None
def check_water_source_empty(now):
return emergency_stop_time and now > emergency_stop_time
# --------- main loop -----------
if GPIO.event_detected(GPIO_PUMP):
is_pouring = is_pump_on()
set_emergency_stop_time(now, is_pouring)
# ...
global pump_disabled
if check_water_source_empty(now):
log_error('[!] Emergency stop of the pump. \
Water source is empty')
toggle_pump(STOP_PUMP)
pump_disabled = True
Slack Notification — Empty Water Bottle
Slack Notification — Empty Water Bottle

Below is an example of a message log generated during an emergency stop.

Smart Pump Operation Log
Smart Pump Operation Log

Running the System 24/7

The code on the device is debugged and runs without problems. We launched it as a service, so it restarts if the Raspberry Pi is rebooted. For convenience, we created a Makefile that helps with deployment, running the service and viewing logs.

.PHONY: install run start stop status log deploy

MAIN_FILE:= coffee-pump/main.py
SERVICE_INSTALL_SCRIPT:= service_install.sh
SERVICE_NAME:= coffee-pump.service

install:
chmod +x $(SERVICE_INSTALL_SCRIPT)
sudo ./$(SERVICE_INSTALL_SCRIPT) $(MAIN_FILE)

run:
sudo python3 $(MAIN_FILE)

start:
sudo systemctl start $(SERVICE_NAME)

status:
sudo systemctl status $(SERVICE_NAME)

stop:
sudo systemctl stop $(SERVICE_NAME)

log:
sudo journalctl -u coffee-pump --since today

deploy:
rsync -av coffee-pump sensor-setup Makefile *.sh pi@XX.XX.XXX.XXX:~/

You can find this file and all the required scripts in our repository.

Cloud Monitoring

We used Cloud4RPi to implement a control panel. We first added widgets to indicate the systems essential parameters.

Cloud4RPi Widgets
Cloud4RPi Widgets

(By the way, the widget for the STATUS variable can use different color schemes based on its value.)

Cloud4RPi — Text Widget Setup
Cloud4RPi — Text Widget Setup

We added a chart widget to display dynamic data. In the image below you can see the moment the pump turned ON and OFF and respective water levels.

Water Level, Pump Relay Turning ON and OFF
Water Level, Pump Relay Turning ON and OFF

If you analyze a longer time span, you can see peaks — that’s when the pump was running.

Cloud4RPi — Time-Series Widget
Cloud4RPi — Time-Series Widget

Cloud4RPi also allows you to set different smoothing levels.

It works! The control panel in its entirety looks as shown below.

Cloud4RPi Control Panel — Smart Pump Monitoring
Cloud4RPi Control Panel — Smart Pump Monitoring

Currently, our automatic pump has been running for several weeks and all we needed to do is replace water bottles. The full code for our project is available in our GitHub repository.

Demo video

Coffee Machine Pump In Action

Bonus

The Incredible Machine
The Incredible Machine
The Assembled Coffee Machine Pump System
The Assembled Coffee Machine Pump System

--

--