Skip to content

leonardpitzu/DoorOpener

 
 

Repository files navigation

DoorOpener for Home Assistant

A secure web interface for controlling smart door openers via Home Assistant — modern glass-morphism UI with visual keypad, per-user PINs, audio feedback, battery monitoring, and comprehensive brute-force protection.

CI Version Python 3.12+

image image

Features

DoorOpener provides a web-based keypad interface to remotely open doors connected to Home Assistant. Users enter their personal PIN on a visual keypad and the system securely communicates with Home Assistant to trigger the door opener.

  • Visual keypad — 3×4 keypad interface with auto-submit
  • Per-user PINs — individual PINs for each user with JSON-based user management
  • Audio feedback — success chimes and failure sounds
  • Battery monitoring — real-time battery level for Zigbee devices
  • Multi-layer security — rate limiting and IP blocking (see Security)
  • Admin UI — user management and log viewer
  • Test mode — safe development without triggering the actual door
  • Entity support — Home Assistant switch, lock, input_boolean, and button entities

Installation

Home Assistant Add-on (recommended)

  1. In Home Assistant go to Settings → Add-ons → Add-on Store → ⋮ → Repositories.
  2. Paste the repository URL:
    https://github.com/leonardpitzu/DoorOpener
    
  3. Find DoorOpener in the store and click Install.
  4. Configure entity_id and admin_password in the add-on options.
  5. Click Start — the panel appears in the sidebar with a door icon.

When running as an add-on you can leave ha_url and ha_token empty — the Supervisor API token is used automatically.

Docker

A Dockerfile.dev is provided for local testing outside Home Assistant:

docker build -f Dockerfile.dev -t dooropener:dev .
cp options.json.example options.json   # edit as needed
docker run -d \
  -v $(pwd)/options.json:/app/options.json:ro \
  -v $(pwd)/users.json:/app/users.json:rw \
  -v $(pwd)/logs:/app/logs \
  -p 6532:6532 dooropener:dev

Or via Compose:

docker compose up -d

Configuration

options.json

All application settings live in a single options.json file. Copy the example and edit:

cp options.json.example options.json
{
  "ha_url": "http://homeassistant.local:8123",
  "ha_token": "your_long_lived_access_token_here",
  "entity_id": "switch.dooropener_zigbee",
  "battery_entity": "sensor.dooropener_zigbee_battery",
  "port": 6532,
  "test_mode": false,
  "admin_password": "change_me_please",
  "max_attempts": 5,
  "block_time_minutes": 5,
  "max_global_attempts_per_hour": 50,
  "session_max_attempts": 3,
  "secret_key": "",
  "session_cookie_secure": false,
  "ha_cert_pem": ""
}
Key Description Default
ha_url Home Assistant base URL (required)
ha_token Long-lived access token (HA → Profile → Security) (required)
entity_id Entity to trigger (switch.*, lock.*, input_boolean.*, or button.*) (required)
battery_entity Battery sensor entity for monitoring auto-derived
port Web server port (overridden by DOOROPENER_PORT env var) 6532
test_mode When true, simulates door actions without calling HA false
admin_password Password for the admin dashboard (required)
max_attempts Failed PIN attempts per IP before blocking 5
block_time_minutes Block duration in minutes 5
max_global_attempts_per_hour Global rate limit across all clients 50
session_max_attempts Failed attempts per browser session before blocking 3
secret_key Flask secret key (leave empty + set FLASK_SECRET_KEY env var instead) ""
session_cookie_secure Set true when running behind HTTPS false
ha_cert_pem Inline PEM certificate for self-signed HA (see below) ""

Environment variables

# Port (optional, overrides options.json)
DOOROPENER_PORT=6532

# Timezone
TZ=Europe/Amsterdam

# Container user/group mapping (linuxserver-style)
PUID=1000
PGID=1000
UMASK=002

# Flask session secret (recommended for production)
FLASK_SECRET_KEY=please-change-me

# Set true when behind HTTPS reverse proxy
SESSION_COOKIE_SECURE=true

Configuration priority: environment variables > options.json defaults.

The image supports PUID, PGID, and UMASK to avoid host-side chown. On startup the entrypoint aligns the runtime user/group to those IDs, ensures /app/logs is writable, then drops privileges via gosu.

Logs

Log Location
Door access / audit log /app/logs/log.txt (bind-mount ./logs:/app/logs)
Gunicorn / access log Container stdout/stderr (docker logs dooropener)

Self-signed certificates

If your Home Assistant uses a self-signed certificate, paste the PEM certificate directly into the ha_cert_pem option.

In options.json:

{
  "ha_cert_pem": "-----BEGIN CERTIFICATE-----\nMIIFTjCCA...\n-----END CERTIFICATE-----"
}

In the HA add-on YAML config (Settings → Add-ons → DoorOpener → Configuration):

ha_cert_pem: |-
  -----BEGIN CERTIFICATE-----
  MIIFTjCCAzagAwIBAgIIAQ3X7arBz...
  -----END CERTIFICATE-----

The certificate is written to a temporary file at startup and used for all Home Assistant API calls.

Note: The hostname in ha_url must match a Subject Alternative Name in the certificate.

Usage

  1. Access interface — visit http://localhost:6532.
  2. Enter PIN — use the visual keypad (4–8 digit PIN).
  3. Auto-submit — the door opens automatically when a valid-length PIN is entered.
  4. Admin access — click the gear icon for the admin dashboard.

Security

Protection layers

Layer Description
IP rate limiting Blocks an IP after max_attempts failures for block_time_minutes
Session rate limiting Blocks a browser session after session_max_attempts failures
Global rate limiting Caps total failed attempts system-wide to max_global_attempts_per_hour
Progressive delay info Exponential back-off metadata (1 s → 16 s) returned to the client
Persistent cookie block Block state survives page reloads via a session cookie
Audit logging Every attempt logged with timestamp, IP, session, user, and result
Input validation PIN format and request body validated before processing
Security headers CSP, X-Frame-Options DENY, HSTS referral, no-sniff, etc.
Bot detection Obvious bot/crawler/spider user-agents are rejected

Security headers

Every response includes:

  • Content-Security-Policy — strict self-only policy
  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • X-XSS-Protection: 1; mode=block
  • Referrer-Policy: strict-origin-when-cross-origin
  • Permissions-Policy — geolocation, camera, microphone disabled
  • Cache-Control: no-store

API endpoints

Method Path Description
GET / Main keypad interface
POST /open-door Validate PIN and trigger door entity
GET /battery Battery level JSON ({"level": 85})
GET /admin Admin dashboard
POST /admin/auth Admin login
GET /admin/check-auth Check admin session
POST /admin/logout Admin logout
GET /admin/logs Retrieve audit logs (admin auth required)
POST /admin/logs/clear Clear logs (admin auth required)
GET /admin/users List users (admin auth required)
POST /admin/users Create user (admin auth required)
PUT /admin/users/<name> Update user (admin auth required)
DELETE /admin/users/<name> Delete user (admin auth required)

Architecture

┌────────────────────────────────────────────────────┐
│                      app.py                        │
│           Flask routes & request handling          │
├──────────────┬──────────────┬──────────────────────┤
│ config.py    │ security.py  │ ha_client.py         │
│ options.json │ RateLimiter  │ HAClient             │
│ loader       │ headers      │ (requests.Session)   │
│ timezone     │ validation   │ trigger / battery    │
├──────────────┴──────────────┴──────────────────────┤
│                  users_store.py                    │
│            JSON-based user management              │
└────────────────────────────────────────────────────┘
Module Responsibility
dooropener/config.py Loads options.json, exposes all settings as module attributes, timezone handling via zoneinfo
dooropener/security.py RateLimiter class (IP / session / global), security headers, bot detection, PIN validation
dooropener/ha_client.py HAClient class wrapping requests.Session — dispatches switch/lock/input_boolean/button services
dooropener/users_store.py Atomic JSON-based user CRUD with usage tracking
dooropener/app.py Flask app, route handlers, audit logging

Development

Prerequisites

  • Python 3.12+
  • An options.json file (copy from options.json.example)

Local setup

python -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txt
cp options.json.example options.json   # edit as needed
python app.py

Running tests

pytest --tb=short -q

With coverage:

pytest --cov=./ --cov-report=term-missing --cov-fail-under=75

Linting & security

ruff check .          # lint
bandit -r . -x tests  # security scan

Test mode

Set "test_mode": true in options.json to test the interface without actually triggering the door in Home Assistant.

CI / CD

The CI pipeline (.github/workflows/ci.yml) runs on every push and PR:

Job Tool Purpose
Tests pytest + pytest-cov Unit/integration tests, 75 % coverage gate
Lint ruff Code style and import checks
Security bandit Static security analysis
Docker docker build Smoke-test the dev container image

Weekly pull requests for dependency updates are configured via .github/dependabot.yml:

  • pip dependencies — every Monday
  • GitHub Actions versions — every Monday

License

This project is licensed under the MIT License — see the LICENSE file for details.

About

A sleek, single-button web portal for triggering any Home Assistant action (like opening a building door), protected by secure PIN authentication. Perfect for apartment buildings, offices, or any scenario where you want easy, controlled access for multiple users.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • Python 52.8%
  • HTML 45.2%
  • Other 2.0%