A web-based garage door controller using a Raspberry Pi, ultrasonic sensor (HC-SR04), and relay module. Integrates with UniFi Protect for automatic licence plate recognition (LPR) to open and close the garage door when authorised vehicles are detected.
- Real-time garage door status monitoring via ultrasonic sensor
- UniFi Protect LPR integration via webhook (auto-open/close on plate detection)
- Authorised plate management with web UI and API
- Plate normalisation (hyphens, spaces, and case are stripped automatically)
- Auto-close countdown (60s) with cancellable WebSocket notifications
- JWT authentication with bcrypt password hashing and rate-limited login
- Dark mode with Sun/Moon toggle (persisted in localStorage)
- React frontend with Tailwind CSS and custom garage door SVG icons
- Timezone-aware timestamps (UTC storage, local display in en-AU)
- WebSocket support for live status updates (auto-detects ws/wss)
- HTTPS support via self-signed certificate for reverse proxy re-encryption
- Optional MQTT integration for Home Assistant / monitoring
- Nginx reverse proxy with HTTP and HTTPS + WebSocket passthrough
- Mobile-responsive design
- Raspberry Pi (tested on Pi 3A+)
- HC-SR04 Ultrasonic Sensor
- Relay Module
- Garage door opener
| Pin | GPIO |
|---|---|
| TRIGGER | 16 |
| ECHO | 26 |
| RELAY | 24 |
- Raspberry Pi OS (Debian-based)
- Python 3.6+
- Node.js 14+ (for building the frontend)
- Nginx
.
+-- backend/
| +-- main.py # FastAPI application
| +-- uwsgi.ini # uWSGI configuration (legacy)
| +-- garage-controller.service # systemd service file (legacy)
| +-- requirements.txt # Python dependencies
+-- src/ # React source (App.js, index.js, etc.)
+-- public/ # Static assets
+-- nginx/
| +-- sites-available # Nginx site configuration
+-- setup/
| +-- install.sh # Automated installation script
| +-- setup_database.sh # Database initialisation script
| +-- LPR_Setup_Guide.docx # UniFi Protect LPR setup guide
+-- package.json # Node.js dependencies
+-- tailwind.config.js # Tailwind CSS configuration
Browser :80 --> Nginx --> uvicorn :8080 --> FastAPI (main.py)
|
UniFi Protect ---> GET /api/lpr/unifi-webhook?plate=ABC123
|
garage.db (SQLite)
- Nginx listens on port 80, proxies HTTP and WebSocket traffic to uvicorn on port 8080
- Uvicorn runs the FastAPI app from
/home/pi/garageController - React build is served from
/home/pi/garageController/frontend - SQLite stores events, authorised plates, and LPR history (timestamps in UTC)
The garage controller integrates with UniFi Protect cameras that support licence plate recognition. When an authorised plate is detected, the garage door opens or schedules an auto-close automatically.
- UniFi Protect detects a licence plate via the G6 Bullet camera
- An Alarm Manager rule fires a webhook to the Raspberry Pi
- The garage controller checks if the plate is authorised
- Door closed + authorised plate = door opens immediately
- Door open + authorised plate = 60-second auto-close countdown starts
- Unauthorised plate = event logged, no action taken
The primary endpoint for UniFi Protect is a simple GET request with the plate as a query parameter:
GET http://192.168.1.143/api/lpr/unifi-webhook?plate=ABC123
A POST endpoint is also available for JSON payloads:
curl -X POST http://192.168.1.143/api/lpr/unifi-webhook \
-H "Content-Type: application/json" \
-d '{"plate": "ABC123"}'- Enable LPR on your camera (Settings > Smart Detections > Licence Plate)
- Register your vehicle as a Known Vehicle via Find Anything
- Create an Alarm in Alarm Manager with Vehicle ID trigger
- Set the action to Custom Webhook with the GET URL above
- See
setup/LPR_Setup_Guide.docxfor detailed instructions
Plate numbers are automatically normalised before storage and comparison. Hyphens, spaces, and lowercase characters are stripped:
ABC-123,abc 123, andABC123all resolve toABC123- This applies to plates added via the web UI, API, and incoming webhooks
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/status |
Current door status, countdown, pending plate |
| POST | /api/toggle |
Toggle the garage door |
| GET | /api/events |
Last 10 status events |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/lpr/unifi-webhook?plate=XXX |
Webhook for UniFi Protect |
| POST | /api/lpr/unifi-webhook |
Webhook (JSON body) |
| POST | /api/lpr/detect |
Manual LPR test |
| GET | /api/lpr/plates |
List authorised plates |
| POST | /api/lpr/plates |
Add authorised plate |
| DELETE | /api/lpr/plates/{plate} |
Remove authorised plate |
| GET | /api/lpr/events?limit=20 |
LPR event history |
| POST | /api/lpr/cancel |
Cancel pending auto-close |
Connect to ws://192.168.1.143/ws for real-time updates including status changes, LPR events, and auto-close countdown notifications.
If Mosquitto is running on localhost:1883, the app publishes to:
| Topic | Payload |
|---|---|
garage/status |
Open or Closed |
garage/lpr/opened |
Plate number |
garage/lpr/close_scheduled |
Plate number |
garage/lpr/auto_closed |
Plate number |
garage/lpr/unauthorized |
Plate number |
If the broker is unavailable, the app starts normally without MQTT.
git clone https://github.com/rdapaz/garageController.git
cd garageControllerchmod +x setup/install.sh
./setup/install.shOn your development machine (with Node.js installed):
npm install
npm run buildTransfer the build folder contents to /home/pi/garageController/frontend on your Raspberry Pi.
Update paths in the following files if needed:
backend/main.py: Update the StaticFiles directory pathnginx/sites-available: Update server_name and proxy_pass port
cd /home/pi/garageController
nohup /home/pi/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8080 > /tmp/uvicorn.log 2>&1 &Note: The legacy uWSGI service (
garage-controller.service) should be stopped and disabled. Only one instance of the app should run at a time to avoid SQLite database locking errors.
If exposing the controller through a reverse proxy with SSL re-encryption (e.g., Kemp LoadMaster), generate a self-signed certificate:
sudo mkdir -p /etc/nginx/ssl
sudo openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout /etc/nginx/ssl/garage.key \
-out /etc/nginx/ssl/garage.crt \
-subj "/CN=gge.ricdeez.com"Add an SSL server block to nginx (/etc/nginx/sites-enabled/default):
server {
listen 443 ssl;
server_name gge.ricdeez.com;
ssl_certificate /etc/nginx/ssl/garage.crt;
ssl_certificate_key /etc/nginx/ssl/garage.key;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ws {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}The frontend automatically detects https: and uses wss:// for WebSocket connections.
- Local access:
http://192.168.1.143 - External access:
https://gge.ricdeez.com(via Cloudflare + Kemp LB) - Default credentials:
admin/changeme(change on first login)
cat /tmp/uvicorn.logsudo tail -f /var/log/nginx/garage-controller-error.logsudo pkill -f uvicorn
cd /home/pi/garageController
nohup /home/pi/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8080 > /tmp/uvicorn.log 2>&1 &
sudo systemctl restart nginxIf you see sqlite3.OperationalError: database is locked, ensure only one instance is running:
sudo ss -tlnp | grep -E ":8000|:8080"
sudo systemctl stop garage-controller && sudo systemctl disable garage-controller
sudo pkill -f uwsgi- Test the endpoint manually:
curl "http://192.168.1.143/api/lpr/unifi-webhook?plate=TEST123" - Check the plate is in the authorised list:
curl http://192.168.1.143/api/lpr/plates - Review LPR events:
curl http://192.168.1.143/api/lpr/events
sudo nmcli connection modify "Your-WiFi-SSID" ipv4.addresses 192.168.1.100/24 ipv4.gateway 192.168.1.1 ipv4.dns "8.8.8.8" ipv4.method manual
sudo nmcli connection down "Your-WiFi-SSID"
sudo nmcli connection up "Your-WiFi-SSID"MIT