A small Flask web UI to control a Nuki Smart Lock via a RaspiNukiBridge instance.
The app:
- Calls the RaspiNukiBridge HTTP API (
/lockState,/lockAction,/list) - Shows live lock status (lock state, door state, battery, timestamp)
- Provides buttons for Lock, Unlock, Unlatch (open door) and Lock'n'Go
- Normalizes the raw state JSON for debugging / integrations
- Supports English and Italian UI with a language switcher
⚠️ This project is intended as an example / personal tool.
It has no authentication by default. Do not expose it directly to the public internet.
- Python 3.9+ (recommended)
- A running RaspiNukiBridge instance (e.g. on a Raspberry Pi)
- A working Nuki lock configured in the bridge
Install Python dependencies with:
pip install -r requirements.txtAll configuration is handled via config.yaml.
An example file is provided as config_example.yaml:
bridge:
host: "127.0.0.1"
port: 8080
nuki:
token: "CHANGE_ME"
id: 123456789
device_type: 0
web:
port: 5000
language: "en"-
Copy the example configuration:
cp config_example.yaml config.yaml
-
Edit
config.yamland set:bridge.hostandbridge.portto your RaspiNukiBridge addressnuki.tokento theserver.tokenfrom your Nuki / bridge configurationnuki.idto thenukiIdreturned by the/listendpointnuki.device_type(usually0for Smart Lock)web.portfor the Flask HTTP portweb.languageto"en"or"it"for the default UI language
-
Make sure
config.yamlis ignored by Git (see.gitignore) and never committed.
From the project directory:
python app.pyBy default, the app listens on:
http://0.0.0.0:<web.port>
For example, if web.port is 5000:
http://raspberrypi:5000/
Listening on 0.0.0.0 makes the app reachable from your local network and/or VPN (depending on your routing / firewall).
Open the web UI in your browser:
http://<your-host>:<web.port>/
You will see:
- Lock state card:
- Lock state
- Door state
- Battery (with color-coded level and critical flag)
- Last update timestamp (converted to Europe/Rome time)
- Action buttons:
- Lock
- Unlock
- Open door (Unlatch)
- Lock'n'Go
- State details card:
- Normalized JSON view (for debugging / integration with other systems)
The UI calls:
GET /api/state→ calls bridge/lockStateand returns JSONPOST /action/<cmd>→ calls bridge/lockActionwith the appropriateactioncode
Internally, the app talks to the RaspiNukiBridge:
-
GET http://<bridge.host>:<bridge.port>/lockState
Query params:nukiId,deviceType,token -
GET http://<bridge.host>:<bridge.port>/lockAction
Query params:nukiId,deviceType,action,token
Action mapping:
1→ unlock2→ lock3→ unlatch (open door)4→ lock'n'go5→ lock'n'go + unlatch (not wired by default, but easy to add)
The UI is fully localized in English and Italian.
The default language is defined in config.yaml:
web:
language: "en" # or "it"The current language can be changed at runtime in two ways:
-
URL parameter
- Force English:
http://<host>:<port>/?lang=en - Force Italian:
http://<host>:<port>/?lang=it
- Force English:
-
Language switcher in the UI
At the top right you will find a small language switcher:
- 🇬🇧 EN
- 🇮🇹 IT
Clicking a button:
- updates the
?lang=...query parameter - reloads the page
- keeps the language consistent for:
- the main page
- actions (
/action/...) /api/statecalls
The actual UI strings are defined in the STRINGS dictionary in app.py.
The app uses a simple in-code dictionary for i18n, so adding a new language is straightforward.
-
Edit
app.pyand extend theSTRINGSdictFind:
STRINGS = { "en": { ... }, "it": { ... }, }
Add a new entry, for example for German:
STRINGS["de"] = { "html_lang": "de", "subtitle": "Secure remote control – instant actions and live lock status. (DE)", "bridge_label": "Bridge:", "error_keyword": "Error", # ...copy all keys from "en" or "it" and translate them... }
Make sure the new language block defines all the keys used by the template and JS
(you can copy the"en"block and translate value by value). -
Add a button in the language switcher in the HTML template
In the header, there is a block like:
<div class="lang-switcher" aria-label="Language"> <button type="button" class="lang-btn" data-lang="en"> <span class="flag">🇬🇧</span> <span>{{ ui.lang_en_label }}</span> </button> <button type="button" class="lang-btn" data-lang="it"> <span class="flag">🇮🇹</span> <span>{{ ui.lang_it_label }}</span> </button> </div>
Add another button, for example:
<button type="button" class="lang-btn" data-lang="de"> <span class="flag">🇩🇪</span> <span>DE</span> </button>
-
(Optional) Set the default language in
config.yamlweb: language: "de"
If present and valid, this will be the default language when
?lang=is not specified.
After these steps, the new language will be available both via ?lang=de in the URL and via the new flag button in the UI.
On a Linux host (e.g. Raspberry Pi) you can run the app as a systemd service.
This repository includes an example unit file:
nukiweb.service.example
It is suitable for a typical Raspberry Pi setup where the project lives in /home/pi/nuki_web and the virtualenv is .venv.
To use it on your system, copy it as a systemd unit:
sudo cp nukiweb.service.example /etc/systemd/system/nukiweb.service
sudo systemctl daemon-reload
sudo systemctl enable nukiweb.service
sudo systemctl start nukiweb.serviceThen check the status with:
sudo systemctl status nukiweb.serviceIf your user or paths differ, adjust nukiweb.service.example before copying.
This app does not implement authentication by itself.
Recommended:
- Run it behind a VPN or behind a reverse proxy with authentication.
- Use HTTPS if you expose it through a reverse proxy (Nginx, Caddy, Traefik, …).
- Do not expose it directly to the public internet on
0.0.0.0:<port>without proper access control. - Keep your
config.yamlprivate and never commit it to source control.
This project is licensed under the MIT License – see the LICENSE file for details.
