I've been blessed with two lovely cats that both seem to have urinary problems. One of them we had to rush to the vet because he had his bladder clogged and was not able to piss anymore. The main symptom was him visiting the litter box and not doing his thing anymore, just struggling, shaking, etc. Now the test results hint that the other cat seems to have the same issue.
You can improve this project by replacing the one load cell in the middle with 4 smaller ones on each corner, might be a bit more expensive and more dificult to wire it up but the stability of the scale will be much better.
This README won't go into details on how to solder or wire the hardware, there are tons of tutorials online and you can read the documentation on each one of them.
A cat litter box monitoring system that tracks visits, weight, waste output, and health patterns for your cats. Configure any number of cats via a single environment variable.
An ESP32-C3 microcontroller with a load cell measures weight changes in real time, publishes events via MQTT, and a Node.js backend processes the data, identifies which cat visited, tracks health metrics, and stores everything in InfluxDB. A React dashboard provides live monitoring, charts, and alerts.
ESP32-C3 + HX711 load cell + SW-420 vibration sensor
│
│ MQTT (litter/events, litter/commands)
▼
Mosquitto broker (Docker)
│
▼
Express backend (Docker)
├── Cat identification (weight-based, auto-adjusting ranges)
├── Health alert engine (6 alert types)
├── Visit tracking + weight history
├── Litter saturation tracking
│
├──► InfluxDB (Docker) — 90-day retention
│
└──► REST API
│
▼
React dashboard (Vite)
├── TanStack Query (live polling)
├── Chart.js (4 chart types)
└── shadcn/ui + Tailwind CSS
| Component | Model | Purpose |
|---|---|---|
| Microcontroller | ESP32-C3-MINI-1 | WiFi + MQTT + sleep management |
| Load cell | + HX711 amplifier | Weight measurement |
| Vibration sensor | SW-420 | Wake-from-sleep trigger |
| Power | 4x AA batteries (6V) → 5V pin | Portable power via AMS1117-3.3 regulator |
This is just how i did it, you can wire them differently, just make sure you also modify the esp32 code as well.
| Component | Pin |
|---|---|
| HX711 DOUT | GPIO 10 |
| HX711 SCK | GPIO 1 |
| SW-420 DO | GPIO 2 |
Cats are identified by their weight during a visit. The ESP32 collects up to 10 weight samples and uses the median to determine cat weight.
Cats are configured via the CATS environment variable in .env:
CATS=piscot:Piscot:4900:4400:5600,bulion:Bulion:6240:5600:7300
# id:displayName:expectedWeightG:weightMinG:weightMaxG
Each entry defines a cat's ID, display name, expected weight, and the weight range used for identification. You can configure any number of cats, the dashboard and backend adapt automatically.
The boundaries between adjacent weight ranges auto-adjust: the midpoint between each cat's rolling average (last 5 visits) becomes the new boundary.
The backend continuously monitors for 6 types of health concerns:
| Alert | Trigger |
|---|---|
| Low waste | 2+ consecutive visits with < 20g waste |
| Long duration | Visit duration > 3x the cat's rolling average |
| High frequency | 4+ visits within 2 hours |
| No visit | No visit for significantly longer than normal interval |
| Weight change | > 300g gained or lost over a week |
| Litter full | Accumulated waste exceeds historical average per clean cycle |
{"status": "CAT_ENTERED", "value": 5100.0}
{"status": "VISIT_COMPLETE", "waste_g": 45.2, "cat_weight_g": 4920.0, "duration_s": 38, "samples": 12}
{"status": "CLEANING_DONE", "value": 0}
{"status": "MINOR_ACTIVITY", "value": 250.0}
{"status": "LITTER_BOX_REMOVED", "value": -3000.0}
{"status": "HEARTBEAT", "value": 45.0}
{"status": "SYSTEM_START", "value": 0}
{"status": "SYSTEM_RESTART", "value": 0}
{"status": "WAKE_UP", "value": 0}
{"status": "FALSE_TRIGGER", "value": 0}
{"status": "GOING_TO_SLEEP", "value": 0}| Command | Effect |
|---|---|
TARE |
Recalibrate scale zero (re-tare with current weight as baseline) |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/state |
Live state, alerts, litter saturation, cat configs |
| GET | /api/stats/:cat |
Today's visits, total waste, avg duration, weight |
| GET | /api/weight/:cat |
30-day weight history with rolling average |
| GET | /api/history/:cat |
Last N visits (default 20) |
| GET | /api/clean/history |
Clean history + current saturation |
| GET | /api/patterns/:cat |
7-day visit frequency + timestamps |
| POST | /api/clean |
Mark litter as manually cleaned |
| POST | /api/tare |
Remote scale recalibration via MQTT |
The React frontend provides:
- Status Bar — sensor online/offline/sleeping indicator, current scale weight, last heartbeat
- Cat Cards — per-cat daily stats (visits, waste, duration, weight) with alert badges
- Charts — waste per visit (bar), weight over time (line), litter saturation (line), visit time distribution (stacked bar)
- Event Log — scrolling list of recent events
- Litter Status — fullness progress bar, accumulated waste, visits since clean, "Mark as Cleaned" and "Recalibrate Scale" buttons
- Dark mode toggle with localStorage persistence
Polling intervals: state every 5s, stats every 30s, history every 1m, charts every 5m.
The ESP32 uses light sleep to conserve battery:
- Vibration sensor (SW-420) triggers a GPIO interrupt → ESP32 wakes up
- Connects to WiFi and MQTT, publishes
WAKE_UP - Monitors the scale for cat visits
- After 30 seconds of inactivity, publishes
GOING_TO_SLEEPand enters light sleep - If no cat was detected, publishes
FALSE_TRIGGERbefore sleeping
False visit protection after cleaning:
- 10-second cooldown after a clean event
- Visits shorter than 15 seconds are discarded
- Cat weight must be at least 2,000g to count
- Docker and Docker Compose
- Node.js 20+ (for frontend dev server)
- Arduino IDE or PlatformIO (for ESP32 flashing)
cp .env.example .env
# Edit .env — configure your cats in the CATS variable
docker-compose up -dThis starts Mosquitto (MQTT broker), InfluxDB, and the backend.
cd frontend
npm install
npm run devThe dashboard will be available at http://localhost:5173.
Open arduinoCodeExample/esp32.c++ in Arduino IDE. Update the network settings:
const char* WIFI_SSID = "YourWiFi";
const char* WIFI_PASSWORD = "YourPassword";
const char* MQTT_SERVER = "192.168.x.x"; // your server IPInstall the required libraries:
- PubSubClient (MQTT)
- HX711 (load cell)
Flash to the ESP32-C3. On first boot, place the litter box on the scale — the system will tare automatically.
If the scale readings drift or the litter box is repositioned:
- Make sure only the litter box is on the scale (no cats)
- Click "Recalibrate Scale" on the dashboard
- The ESP32 will re-tare and save the new zero offset
A dedicated calibration sketch is available at arduinoCodeExample/calibrate.c++ for finding the correct CALIBRATION_FACTOR with a known weight.
| Variable | Default | Description |
|---|---|---|
INFLUXDB_ORG |
smartlitter |
InfluxDB organization |
INFLUXDB_BUCKET |
litter |
InfluxDB bucket name |
INFLUXDB_TOKEN |
— | InfluxDB admin token |
INFLUXDB_USERNAME |
admin |
InfluxDB admin user |
INFLUXDB_PASSWORD |
— | InfluxDB admin password |
INFLUXDB_URL |
http://localhost:8086 |
InfluxDB connection URL |
MQTT_BROKER |
mqtt://localhost:1883 |
MQTT broker URL |
MQTT_TOPIC |
litter/events |
MQTT event topic |
CATS |
(see below) | Cat config: id:displayName:expectedWeightG:minG:maxG comma-separated |
VITE_API_URL |
http://localhost:3000 |
Frontend API base URL |
smartLitterBox/
├── docker-compose.yml
├── .env
├── mosquitto/config/mosquitto.conf
├── backend/
│ ├── Dockerfile
│ ├── package.json
│ └── src/
│ ├── server.ts # Entry point: MQTT + Express
│ ├── env.ts # dotenv loader
│ ├── types.ts # Shared types
│ ├── catIdentification.ts # Weight-based cat ID
│ ├── state.ts # In-memory live state
│ ├── eventHandler.ts # MQTT event processing
│ ├── healthAlerts.ts # Health alert checks
│ ├── influx.ts # InfluxDB read/write
│ └── routes.ts # REST API routes
├── frontend/
│ ├── package.json
│ ├── vite.config.ts
│ └── src/
│ ├── main.tsx # App entry + QueryClient
│ ├── App.tsx # Layout
│ ├── api/client.ts # API fetch functions
│ ├── types/index.ts # Frontend types
│ ├── hooks/
│ │ ├── useQueries.ts # TanStack Query hooks
│ │ ├── useEventLog.ts # Event log state
│ │ └── useTheme.ts # Dark mode
│ └── components/
│ ├── StatusBar.tsx # Sensor status
│ ├── CatCard.tsx # Per-cat stats
│ ├── Charts.tsx # 4 Chart.js charts
│ ├── EventLog.tsx # Recent events
│ ├── LitterStatus.tsx # Litter saturation
│ └── ThemeToggle.tsx # Dark mode toggle
└── arduinoCodeExample/
├── esp32.c++ # ESP32 firmware
└── calibrate.c++ # Scale calibration tool
| Layer | Technology |
|---|---|
| Microcontroller | ESP32-C3 (Arduino/C++) |
| Message broker | Mosquitto 2.0 (MQTT) |
| Backend | Express + TypeScript (Node.js 20) |
| Database | InfluxDB 2.7 (90-day retention) |
| Frontend | React 19 + Vite 7 |
| UI | shadcn/ui + Tailwind CSS 4 |
| Charts | Chart.js 4 + react-chartjs-2 |
| Data fetching | TanStack Query 5 |
| Containerization | Docker Compose |



