How to Automate Aquarium Water Changes with a Python System

Maintaining a healthy aquarium requires regular water changes, but manually siphoning water, mixing new water, and refilling the tank is time-consuming and easy to forget. In this tutorial, I’ll show you how to build a Python‑based automated aquarium water change system that handles the entire process—draining old water, preparing fresh water with proper temperature and chemistry, and refilling the tank—all with just a few lines of code and basic hardware.

Why Automate Aquarium Water Changes?

Manual water changes are prone to human error:

  • Inconsistent timing (missing weekly or monthly changes disrupts water parameters)
  • Incorrect water temperature (shocks fish)
  • Incorrect water volume (risk of over‑draining or over‑filling)
  • Forgotten salt or water conditioner doses (critical for marine and brackish tanks)

A Python automation system solves these issues by standardizing the process, running on a schedule, and validating water conditions before any changes—keeping your aquatic life happy and healthy with minimal effort.

What You’ll Need

Hardware

Component

Purpose

Budget Estimate

Raspberry Pi (3/4/Zero)

The “brain” to run Python code

$30–$50

Submersible Water Pumps

2 pumps (1 for draining, 1 for refilling)

$15–$25 each

Temperature Sensor (DS18B20)

Monitor fresh water temperature (requires 1‑Wire enabled)

$5–$10

Float Sensors

Prevent over‑draining the tank or over‑filling during refilling

$8–$12 each

Relay Module (5V)

Control pumps (Python cannot drive pumps directly)

$5–$8

Power Supply

Power Pi (5V/2.5A) and pumps

$10–$15

Tubing & Fittings

Connect pumps to tank and fresh water reservoir

$10–$20

Water Conditioner/Salt

For treating fresh water (as needed)

$5–$10

Software

  • Python 3.7+ (pre‑installed on Raspberry Pi OS)
  • Libraries: RPi.GPIO, schedule, time, ds18b20

Install the required packages:

pip install RPi.GPIO schedule ds18b20

Step 1: System Design & Wiring

The core logic of the system is straightforward:

  1. Drain Phase: Activate the drain pump until the float sensor reaches the target low level.
  2. Fresh Water Prep: Check that fresh water temperature matches the tank (±1°C).
  3. Refill Phase: Activate the refill pump until the tank is full.
  4. Safety Checks: Stop all pumps if sensors detect errors (empty reservoir, wrong temperature, etc.).

Basic Wiring Guide

  • Drain pump → Relay 1 → GPIO 17
  • Refill pump → Relay 2 → GPIO 27
  • Low‑level float sensor → GPIO 22
  • High‑level float sensor → GPIO 23
  • Fresh water reservoir sensor → GPIO 24
  • DS18B20 temperature sensor → GPIO 4 (enable 1‑Wire in raspi-config)

Step 2: Python Code for Automated Water Changes

This code includes safety checks, scheduled runs, and automatic level control.

import RPi.GPIO as GPIO
import schedule
import time
from ds18b20 import DS18B20

 
# --------------------------
# GPIO Pin Configuration
# --------------------------
DRAIN_PUMP_PIN = 17
REFILL_PUMP_PIN = 27

 
LOW_LEVEL_SENSOR_PIN = 22
HIGH_LEVEL_SENSOR_PIN = 23
FRESH_WATER_SENSOR_PIN = 24

 
TEMP_SENSOR = DS18B20()
TARGET_TEMP = 25.0
TEMP_TOLERANCE = 1.0

 
# --------------------------
# GPIO Setup
# --------------------------
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)

 
GPIO.setup(DRAIN_PUMP_PIN, GPIO.OUT)
GPIO.setup(REFILL_PUMP_PIN, GPIO.OUT)

 
GPIO.setup(LOW_LEVEL_SENSOR_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(HIGH_LEVEL_SENSOR_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(FRESH_WATER_SENSOR_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)

 
# --------------------------
# Core Functions
# --------------------------
def read_temperature():
    try:
        temp = TEMP_SENSOR.get_temperature()
        return round(temp, 1)
    except Exception as e:
        print(f"Error reading temperature: {e}")
        return None

 
def check_safety_conditions():
    if GPIO.input(FRESH_WATER_SENSOR_PIN) == 0:
        print("ERROR: Fresh water reservoir is empty!")
        return False

 
    if GPIO.input(LOW_LEVEL_SENSOR_PIN) == 0:
        print("ERROR: Tank water level is already too low!")
        return False

 
    fresh_temp = read_temperature()
    if fresh_temp is None:
        print("ERROR: Failed to read temperature!")
        return False

 
    if not (TARGET_TEMP - TEMP_TOLERANCE <= fresh_temp <= TARGET_TEMP + TEMP_TOLERANCE):
        print(f"ERROR: Fresh water temp ({fresh_temp}°C) is outside target range!")
        return False

 
    return True

 
def drain_water():
    print("Starting drain phase...")
    GPIO.output(DRAIN_PUMP_PIN, GPIO.HIGH)
    count = 0
    while GPIO.input(LOW_LEVEL_SENSOR_PIN) == 1:
        time.sleep(1)
        count += 1
        if count % 5 == 0:
            print("Draining...")
    GPIO.output(DRAIN_PUMP_PIN, GPIO.LOW)
    print("Drain phase complete!")

 
def refill_water():
    print("Starting refill phase...")
    GPIO.output(REFILL_PUMP_PIN, GPIO.HIGH)
    count = 0
    while GPIO.input(HIGH_LEVEL_SENSOR_PIN) == 1:
        time.sleep(1)
        count += 1
        if count % 5 == 0:
            print("Refilling...")
    GPIO.output(REFILL_PUMP_PIN, GPIO.LOW)
    print("Refill phase complete!")

 
def full_water_change():
    print("=== Starting Automated Water Change ===")
    if not check_safety_conditions():
        print("Aborting water change due to safety issues!")
        return

 
    drain_water()
    print("Adding water conditioner / salt (10s delay)...")
    time.sleep(10)
    refill_water()

 
    final_temp = read_temperature()
    print("=== Water Change Complete! ===")
    print(f"Final tank temp: {final_temp}°C")
    print("Pumps off. All sensors normal.")

 
def cleanup():
    GPIO.output(DRAIN_PUMP_PIN, GPIO.LOW)
    GPIO.output(REFILL_PUMP_PIN, GPIO.LOW)
    GPIO.cleanup()
    print("GPIO cleaned up safely.")

 
# --------------------------
# Schedule & Run
# --------------------------
if __name__ == "__main__":
    try:
        schedule.every().sunday.at("09:00").do(full_water_change)
        print("Automated Aquarium Water Change System Running...")
        print("Scheduled: Every Sunday at 09:00")
        print("Press Ctrl+C to stop.")

 
        while True:
            schedule.run_pending()
            time.sleep(60)

 
    except KeyboardInterrupt:
        print("\nSystem stopped by user.")
        cleanup()
    except Exception as e:
        print(f"Unexpected error: {e}")
        cleanup()

Step 3: Key Code Explanations

GPIO Configuration

We use internal pull‑up resistors to avoid false sensor readings, which is critical for stable operation.

Safety Checks

The system verifies:

  • Fresh water reservoir is not empty
  • Tank level is safe before draining
  • Water temperature is within safe range

Scheduling

You can easily change how often the system runs:

  • Daily: schedule.every().day.at("18:00")
  • Every 7 days: schedule.every(7).days.do(full_water_change)

Cleanup

The cleanup() function ensures pumps turn off if the script stops, preventing flooding or burnout.

Step 4: Testing & Customization

  • Test in dry mode first (without water) to verify sensors and logic.
  • Adjust TARGET_TEMP and TEMP_TOLERANCE for your fish.
  • Extend the 10‑second delay if you need more time to add conditioner.
  • Add logging to track history and debug issues.

Advanced Upgrades

  • WiFi monitoring with Flask or MQTT
  • Automatic dosing pump for salt/conditioner
  • Leak detection sensors
  • UPS for power failure protection

Summary

This Python & Raspberry Pi system fully automates aquarium water changes with reliable safety controls. The workflow is:

Safety check → Drain → Treat water → Refill

By running on a fixed schedule, you eliminate human error and save hours of maintenance. Always test thoroughly before leaving the system unattended.

Happy automating!