Skip to content
 
 

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 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

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages