Skip to content

Repository files navigation

Door Opener

A small web app that opens your office door from a phone or laptop by sending an unlock command to a ZKTeco access device over the local network. Tapping the button fires the same door relay that a successful fingerprint match does.

It serves a passcode login, then a single "Open Door" button with live device status.

How it works

Phone / laptop browser  --->  this web app  --->  ZKTeco device  --->  door relay
        (HTTP)            (LAN, port 4370 via pyzk)

The app runs on any always-on machine on the same network as the device (a Raspberry Pi, a mini PC, or a spare computer). It connects to the device using the pyzk library and calls the unlock command.

Requirements

  • Python 3.9 or newer
  • A machine on the same local network as the ZKTeco device
  • The device's IP address, port, and (if set) communication password
  • The device must have its door relay wired to the lock (the same wiring that already lets a fingerprint open the door)

Setup

# 1. Create and activate a virtual environment
python3 -m venv venv
source venv/bin/activate        # Windows: venv\Scripts\activate

# 2. Install dependencies
pip install -r requirements.txt

# 3. Create your config
cp .env.example .env

# 4. Generate a secret key and paste it into .env as SECRET_KEY
python -c "import secrets; print(secrets.token_hex(32))"

# 5. Edit .env: set ACCESS_PASSCODE, DEVICE_IP, and any device password

Configuration

All settings live in .env. See .env.example for the full annotated list. The device settings (IP, port, password, timeout, UDP/ping, auto-discovery, discovery subnets, door-open seconds) can also be set live from the Device panel in the app (footer → Device); those overrides are saved to device_config.json (gitignored) and take effect without editing .env or restarting. The panel also has Scan (find the device on the network) and Test (check the connection) buttons. The values below are the defaults the app falls back to:

Variable What it is
SECRET_KEY Random key that signs login cookies. Generate one.
ACCESS_PASSCODE The passcode your team types to log in.
DEVICE_IP The device's IP on your network.
DEVICE_PORT Device port, usually 4370.
DEVICE_PASSWORD Device communication password, 0 if none.
DOOR_OPEN_SECONDS How long the lock stays released per tap.
TRUST_PROXY False by default. Set True only behind a trusted reverse proxy.
SESSION_COOKIE_SECURE False for HTTP LAN. Set True when fronted by HTTPS.

Run

For quick local testing:

python app.py

Then open http://<machine-ip>:8080 on a phone or laptop on the same network.

Production

See DEPLOY.md for the full production guide (architecture, systemd, HTTPS proxy, Windows, firewall, secrets). In short:

pip install -r requirements.txt
gunicorn -c gunicorn.conf.py app:app        # Linux
# or on Windows:
waitress-serve --listen=0.0.0.0:8080 --threads=8 app:app

One worker, multiple threads. In-memory state (rate-limit, status, discovery) and the config files are per-process, so run a single worker; the gthread worker's threads keep a live camera stream from blocking the door. gunicorn.conf.py is configured this way. A deploy/door-opener.service systemd unit and a deploy/Caddyfile (HTTPS) are included.

It's a LAN appliance. The server must be on the same network as the device/camera, one instance per site. Don't expose it to the public internet; use a VPN or the HTTPS-proxy mode in DEPLOY.md.

Endpoints

Route Method Purpose
/healthz GET Liveness probe (no auth, no device I/O)
/login GET/POST Passcode login
/ GET The open-door page (requires login)
/open POST Sends the unlock command (CSRF protected)
/status GET Checks whether the device is reachable
/logout POST Ends the session
/device/config GET/POST Read / save device settings (POST is CSRF protected)
/device/scan POST Scan the network for the device (CSRF protected)
/device/test POST Test the connection to the device (CSRF protected)
/camera/config GET/POST Read / save camera settings (POST is CSRF protected)
/camera/snapshot GET One JPEG frame (proxied from the camera)
/camera/stream GET Live MJPEG feed (proxied / transcoded)

Camera (see who's at the door)

The open-door page shows a live camera panel above the button. Click Connect to add a network camera; settings are saved to camera_config.json (gitignored) and persist across restarts. Three modes:

Mode URL example Needs
snapshot http://192.168.1.50/snapshot.jpg nothing - universal, polled ~1/sec
mjpeg http://192.168.1.50/video.mjpg nothing - live HTTP MJPEG
rtsp rtsp://user:pass@192.168.1.50:554/stream1 ffmpeg on PATH - transcoded to live MJPEG

The feed is proxied through the app, so the camera's credentials never reach the browser and the camera stays on the LAN. Optional username/password are stored in camera_config.json and sent to the camera using basic or digest auth.

Notes:

  • For RTSP, install ffmpeg and ensure it's on PATH (the form warns if missing).
  • Snapshot mode works with almost any IP camera (Hikvision ISAPI picture URL, Dahua /cgi-bin/snapshot.cgi, etc.) and is the lightest option.
  • The camera URL host must be a private/LAN address unless CAMERA_ALLOW_PUBLIC=True (an SSRF guard, since the URL is user-supplied).
  • CAMERA_SNAPSHOT_INTERVAL_MS controls the refresh rate of snapshot mode.

Security notes

  • This app should run on your local network only. Do not forward a port on your router to expose it to the open internet.
  • To open the door while away from the office, connect to the office network with a VPN first (for example WireGuard or Tailscale), then open the app as if you were on site. This keeps the door opener off the public internet.
  • Logins are protected by a shared passcode, signed session cookies, a CSRF token on the open command, a per-IP open cooldown, and login-attempt throttling. These are sensible for a LAN, not a substitute for keeping the app off the public internet.
  • Rate limiting keys on the socket peer IP (request.remote_addr). The X-Forwarded-For header is ignored unless TRUST_PROXY=True, so a client cannot spoof it to bypass throttling. Set TRUST_PROXY=True only when the app sits behind a single trusted reverse proxy that sets that header.
  • If you front the app with HTTPS (a TLS proxy or VPN gateway), set SESSION_COOKIE_SECURE=True so the session cookie is only sent over TLS.
  • Responses send X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy, and a Content-Security-Policy to limit clickjacking and content-sniffing.
  • Never commit .env. It is already gitignored.

Troubleshooting

  • Device unreachable / connection hangs: confirm the machine can reach the device IP (ping <DEVICE_IP> or check the router). If ping is blocked but the device is up, keep DEVICE_OMMIT_PING=True.

  • Connects then fails: try DEVICE_FORCE_UDP=True in .env. Some firmware prefers UDP.

  • Wrong / unknown IP: check the device under Menu > Comm > Ethernet, or let the app find it. Run the bundled scanner from the project root:

    python zk_door.py

    It scans the subnets in DEVICE_DISCOVERY_SUBNETS (default 192.168.0 and 192.168.1) and prints the device's IP. Set that as DEVICE_IP, or set DEVICE_AUTODISCOVER=True to let the running app discover and cache it automatically. Discovery only works when the machine running this app is on the same subnet as the device - if the device is on 192.168.0.x and your machine is on 192.168.1.x with no route between them, no scan can reach it; run the app on a host that shares the device's network.

  • Separate controller box: if the unit you touch is only a reader and a separate controller (for example a ZKAccess C3 or inBio) drives the lock, this app talks to the wrong box. In that case the unlock call goes to the controller instead, using a different library (pyzkaccess or zkaccess-c3). Tell me and I will adapt zk_door.py for that hardware.

Project layout

door-opener/
├── app.py            # Flask app: routes, login, rate limiting
├── config.py         # Loads settings from .env
├── zk_door.py        # Talks to the ZKTeco device (open_door, ping_device, discovery)
├── device_store.py   # Runtime device settings (in-app Device panel) over .env
├── camera.py         # Network camera config + snapshot/stream proxy
├── templates/
│   ├── login.html    # Passcode login screen
│   └── index.html    # Open-door button + live status
├── requirements.txt
├── .env.example      # Copy to .env and fill in
├── .gitignore
└── README.md

About

A small web app that opens your office door from a phone or laptop by sending an unlock command to a ZKTeco access device over the local network. Tapping the button fires the same door relay that a successful fingerprint match does.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages