Note
This project was originally forked from roquerodrigo/ha-ttlock-ble and diverged to support expanded Bluetooth passage mode scheduling, auto-lock controls, and on-device credential synchronization.
Local control of TTLock smart locks over Bluetooth, for Home Assistant. Lock / unlock, battery level and real-time push events flow over BLE — no cloud round-trip on every operation. Built on the Python SDK ttlock-ble.
- Local BLE control — lock, unlock, and state queries run over the lock's BLE link; the TTLock cloud is only contacted once at setup to download per-lock keys.
- Passage Mode management — configure single or multi-interval schedules (daily, weekly, specific days) on the lock over Bluetooth. Includes quick dashboard toggle (
switch), real-time active status (binary_sensor) with 0% lock battery impact (exact boundary timestamps), and dynamic upcoming schedule inspection (sensor). - Auto-Lock configuration — customize relock delays directly from Home Assistant using a delay slider (
number, 0 to 900s) or enable/disable toggle (switch). - Enrolled Credential counts — diagnostic sensors reporting the total number of PIN passcodes, RFID/IC cards, and biometric fingerprints enrolled on the lock. Locks without fingerprint sensors report 0 gracefully.
- Detailed Unlock History — human-readable last unlock method sensor (
Fingerprint,Passcode,RFID Card,Mobile App,Auto-Lock,Mechanical Key, etc.) with sequence number, timestamp, credential ID, and user ID attributes. - Action buttons — trigger on-demand Bluetooth syncs directly from the dashboard: calibrate hardware clock, sync operation log, refresh state, sync passcodes, sync cards, sync fingerprints, and sync passage mode schedules.
- Rich Bluetooth Actions (Services) — programmatic access to configure or clear passage schedules, query auto-lock limits, read lock hardware clock, fetch operation log records with date filtering, and query enrolled passcodes, cards, and fingerprints.
- State persistence — sensors preserve counts, unlock history, and schedules across Home Assistant restarts and integration reloads (
RestoreEntity). - Real-time push events — keypad presses, fingerprint reads, IC-card swipes, mechanical key turns, and auto-lock fires arrive as Home Assistant events the moment the lock emits them.
- Battery sensor — diagnostic entity refreshed by every poll and every push, no extra BLE traffic.
- Clock alignment — the lock's own clock, which stamps every operation-log record, is compared against local time once a day on a session something else already opened, and corrected when it has wandered. A diagnostic sensor reports the measured drift.
- Bluetooth discovery — a lock in range of any radio Home Assistant manages shows up on its own as a discovered device; the address and the frame header come off the advertisement, so only the keys are left to provide.
- 2FA-aware config flow — handles TTLock's "new device" verification by emailing a one-time code and prompting for it.
- Works without a cloud account — a lock initialised locally can be added by entering its key directly, no TTLock account involved at any point.
- Passive state tracking — the bolt position and battery level are read from the lock's own BLE advertisements, with no connection and no battery cost. This is what catches an auto-lock or an operation done from the official app.
- Connects only on demand — nothing is held open. A session is opened when a command needs the lock, when the lock advertises that it has records worth reading, or when the permanent connection option asks for one.
- Reauth + reconfigure — re-prompt for credentials in place when the cloud rejects the cached login, or edit them via the integration's three-dot menu.
- Diagnostics — downloadable dump with credentials/keys redacted.
- Translations — English and Brazilian Portuguese (parity enforced by tests).
Each configured lock produces one HA device, carrying up to 23 entities across 7 domains:
| Entity | Domain | Category | Purpose |
|---|---|---|---|
lock.<alias> |
lock |
Control | Locked/unlocked state, with optimistic updates, locking/unlocking transitional states and a post-command settle window. |
switch.<alias>_passage_mode |
switch |
Config | Quick toggle to turn passage mode on or off. Turning ON configures passage mode on the lock; turning OFF clears passage mode. |
switch.<alias>_auto_lock |
switch |
Config | Enable or disable auto-relocking. Disabling sets delay to 0; enabling restores previous delay. |
switch.<alias>_sound |
switch |
Config | The lock's keypad/lock beep (admin keys only). |
number.<alias>_auto_lock_time |
number |
Config | Auto-lock delay duration in seconds (0 disables auto-lock). |
binary_sensor.<alias>_passage_mode_active |
binary_sensor |
Diagnostic | Real-time state whether passage mode is currently holding the door unlocked (on/off). Evaluates schedules in HA memory at exact boundary timestamps with 0% battery drain on the lock. |
binary_sensor.<alias>_connection |
binary_sensor |
Diagnostic | Whether a BLE session is open right now. A healthy idle lock holds none, so off means idle, not unreachable. |
sensor.<alias>_battery |
sensor |
Diagnostic | Battery percentage. |
sensor.<alias>_last_seen |
sensor |
Diagnostic | When the lock was last heard from, read from HA's own advertisement history. |
sensor.<alias>_clock_drift |
sensor |
Diagnostic | Drift of the lock's internal hardware clock against local time, in seconds. |
sensor.<alias>_last_unlock_method |
sensor |
Diagnostic | Formatted last unlock operator (e.g. Fingerprint (66051), Passcode, RFID Card (12345), Mobile App, Auto-Lock, Mechanical Key) with sequence number, credential ID, and timestamp attributes. |
sensor.<alias>_passage_mode_schedule |
sensor |
Diagnostic | Dynamic passage status (Active (until HH:MM), Next: Today HH:MM, or No schedule) with today_slots and full schedule list attributes. |
sensor.<alias>_passcodes_count |
sensor |
Diagnostic | Total number of PIN passcodes stored on the lock chip. |
sensor.<alias>_cards_count |
sensor |
Diagnostic | Total number of RFID / IC cards enrolled on the lock chip. |
sensor.<alias>_fingerprints_count |
sensor |
Diagnostic | Total number of enrolled fingerprints on the lock chip (0 if lock lacks fingerprint hardware). |
button.<alias>_sync_clock |
button |
Config | Calibrate the lock's hardware clock to Home Assistant local time. |
button.<alias>_sync_log |
button |
Diagnostic | Sync new operation-log records from the lock. |
button.<alias>_refresh_state |
button |
Diagnostic | Force an immediate Bluetooth query to update lock state and battery. |
button.<alias>_sync_passcodes |
button |
Diagnostic | Query programmed passcodes over Bluetooth in the background and update the passcodes count sensor. |
button.<alias>_sync_cards |
button |
Diagnostic | Query enrolled IC/RFID cards over Bluetooth in the background and update the cards count sensor. |
button.<alias>_sync_fingerprints |
button |
Diagnostic | Query enrolled fingerprints over Bluetooth in the background and update the fingerprints count sensor. |
button.<alias>_sync_passage_mode |
button |
Diagnostic | Query passage mode schedules over Bluetooth in the background and update passage sensors. |
event.<alias>_log |
event |
— | Fires for every new operation-log record read from the lock. |
The event entity classifies each record as unlock, lock, unlock_failed, password_change or other, and attaches record_type and battery always, plus timestamp, uid, credential, key_id and accessory_battery when the record carries them. credential is only populated for record types where the value is an identifier (card number, fingerprint id, fob MAC) — record types where it would be a working door code never expose it.
The integration registers custom actions under the hass_ttlock_ble domain to inspect, configure, and manage lock settings over Bluetooth.
| Action | Description | Target |
|---|---|---|
hass_ttlock_ble.set_passage_mode |
Configure one or more passage mode intervals on the lock. Supports slots (list of schedules), start_time, end_time, days (or everyday), all_day, and clear_existing. |
Lock entity / device |
hass_ttlock_ble.get_passage_mode |
Read all configured passage mode schedule intervals directly from lock memory over Bluetooth. | Lock entity / device |
hass_ttlock_ble.delete_passage_mode |
Delete a specific passage mode interval by start time, end time, and day. | Lock entity / device |
hass_ttlock_ble.clear_passage_mode |
Remove all configured passage mode schedule intervals from the lock. | Lock entity / device |
hass_ttlock_ble.get_auto_lock_time |
Query current auto-lock duration in seconds and the lock hardware's supported min/max limits. | Lock entity / device |
hass_ttlock_ble.get_lock_time |
Query the lock's internal real-time hardware clock and compute current drift relative to Home Assistant local time. | Lock entity / device |
hass_ttlock_ble.get_operation_log |
Fetch recent operation-log records from the lock's on-chip memory. Supports max_entries, from_sequence, to_sequence, start_date, and end_date. |
Lock entity / device |
hass_ttlock_ble.get_passcodes |
Query all programmed keyboard passcodes (PINs), passcode types, and validity periods. | Lock entity / device |
hass_ttlock_ble.get_cards |
Query all enrolled RFID / IC cards, card numbers, and validity periods. | Lock entity / device |
hass_ttlock_ble.get_fingerprints |
Query all enrolled biometric fingerprints, IDs, and validity periods. | Lock entity / device |
action: hass_ttlock_ble.set_passage_mode
target:
entity_id: lock.front_door
data:
clear_existing: true
slots:
- start_time: "08:00"
end_time: "17:00"
days: "everyday"
- start_time: "13:00"
end_time: "15:00"
days: "friday"- Install via HACS using the button above, or add this repo as a custom HACS repository (category: Integration).
- Restart Home Assistant.
- Settings → Devices & Services → either pick the TTLock BLE card already waiting under Discovered (any lock in range of a radio Home Assistant manages announces itself), or Add Integration → TTLock BLE.
- Choose how the keys are obtained:
- Sign in to a TTLock account — enter the email + password you use in the official app. If TTLock has never seen this Home Assistant before it emails a verification code; paste it into the next step. Every lock on the account is synced at once.
- Enter a lock key manually — for a lock initialised outside the cloud. See below.
- From this point on, all lock / unlock / state operations stay on Bluetooth.
A lock initialised by a local BLE bridge never goes through the cloud, and its owner already holds what Bluetooth needs. That key can be entered directly, one lock per entry:
| Field | Notes |
|---|---|
| Lock MAC address | AA:BB:CC:DD:EE:FF |
| AES key | 16 bytes, continuous hex or separated (2c,3d,…) |
| Unlock key | digits, entered verbatim — not the obfuscated form the cloud returns |
| Admin passcode | optional, digits; only needed for managing passcodes |
| Protocol type / version / scene / group ID / organisation ID | the frame header the lock expects. The defaults (5, 3, 2, 1, 1) suit most V3 locks |
Started from a discovered lock, the MAC address, the name and the first three protocol numbers arrive pre-filled from the advertisement — the keys are all that is left to type.
Only these reach the wire. The rest of what the cloud returns per key — user id, lock-flag position, validity window — is never read by the Bluetooth layer, which addresses the lock with a zeroed user id and the firmware's "permanent key" date literals regardless.
If the lock is in range when the form is submitted, the protocol type, version and scene are checked against what it broadcasts, so a wrong value is caught there instead of becoming a lock that never answers. Getting a value wrong later is fixable through the integration's three-dot menu → Reconfigure.
Reaching the lock takes no extra configuration: whatever Bluetooth radio Home Assistant already manages — USB dongle, built-in adapter, or an ESPHome proxy — is the one used.
Settings → Devices & Services → TTLock BLE → Configure exposes one setting:
permanent_connection(default off) — hold a BLE session open, reopening it the instant the lock drops it. Push events (keypad, auto-lock) then arrive in real time, and the lock's battery pays for it: the firmware closes an idle session within seconds, so this reconnects about as often. With the option off nothing is held open and nothing reconnects.
There is no polling interval and no reconnect pacing to configure. Earlier releases exposed scan_interval and reconnect_interval; both are gone, because nothing polls on a schedule any more — state arrives from the lock's own advertisements, and a session is opened only when something actually needs one.
To edit credentials without removing and re-adding, use the integration's three-dot menu → Reconfigure.
The lock's TTLock firmware aggressively closes idle BLE sessions (~5 s of silence and it drops). The integration:
- Reads the bolt position and battery level straight out of the lock's BLE advertisements. This costs no connection at all and is the only channel that reports an operation performed while Home Assistant is not connected — an auto-lock, the official app, a keypad code.
- Opens a session only when there is a reason to: a
lock/unlockcommand, an advertisement announcing unsynced operation records, or the first bolt position while none is known at all. Withpermanent_connectionon,connection.pyalso holds one open and reopens it on every drop bleak signals. - After a user-initiated
lock/unlock, the SDK keeps the link alive for 25 s so push events (the lock's reports of keypad operations, auto-locks, etc.) reach Home Assistant in real time. - Each push event carries the decoded
lock_stateandbatterywhen the firmware emits a heartbeat-style payload, letting the entities update without a follow-up query. - While a session is open anyway, the lock is asked what it is — model, hardware and firmware — and the answer is remembered, so a restart shows it without connecting.
scripts/setup # install dependencies (uv sync, dev + lint groups)
scripts/develop # start Home Assistant in debug mode with the integration loaded
# Lint and test directly (config lives in pyproject.toml):
uv run ruff format --check .
uv run ruff check .
uv run mypy custom_components/hass_ttlock_ble
uv run pytestHA runs with config in config/ and PYTHONPATH pointing at the repo root. To reset entity/device IDs during development:
rm config/.storage/core.entity_registry config/.storage/core.device_registrycustom_components/hass_ttlock_ble/
├── __init__.py # config-entry lifecycle & service registrations
├── advertisement.py # TtlockBleAdvertisementTracker: state from advertisements
├── api.py # TtlockBleApiClient: TTLockCloud wrapper (cloud bootstrap only)
├── binary_sensor.py # BLE connection and passage mode active binary sensors
├── brand/ # icon / logo PNGs (local placeholder for HA brand registry)
├── button.py # Action buttons (clock sync, log sync, state refresh, credential sync)
├── config_flow.py # menu / bluetooth / cloud / manual / verify_code / reauth / reconfigure
├── connection.py # TtlockBleConnection: persistent BLE session per lock
├── const.py # DOMAIN, LOGGER, service constants, defaults
├── coordinator.py # DataUpdateCoordinator polling each connection
├── credentials.py # BLE exchange handlers for passcodes, cards, and fingerprints
├── data/ # one TypedDict/dataclass per file + type aliases in __init__.py
├── device_description_store.py # per-lock model / hardware / firmware, persisted
├── diagnostics.py # redacted credentials/keys
├── entity.py # base CoordinatorEntity with DeviceInfo
├── event.py # TtlockBleLogEvent: operation-log records as HA events
├── exceptions/ # one file per exception class
├── lock.py # TtlockBleLock: LockEntity backed by the connection
├── manifest.json # integration metadata and dependencies
├── manual_key.py # TtlockBleManualKey: key entry for cloud-less locks
├── number.py # TtlockBleAutoLockTimeNumber: auto-lock delay slider
├── options_flow.py # TtlockBleOptionsFlow: permanent_connection
├── passage.py # BLE frame builders and parsers for passage mode
├── clock_sync_store.py # persisted clock comparison per lock
├── record_store.py # persisted operation-log cursor per lock
├── sensor.py # battery, last-seen, clock drift, unlock method, credential count sensors
├── services.py # implementations for all custom ttlock_ble actions
├── services.yaml # action schemas, field descriptors, and selectors
├── switch.py # sound, auto-lock, and passage mode switches
└── translations/
├── en.json
└── pt-BR.json
Conventions for contributors live in CODE_STYLE.md; architectural notes for AI agents in CLAUDE.md.
Install once per clone (after scripts/setup):
pre-commit installThis wires ruff, mypy and basic file hygiene checks (.pre-commit-config.yaml) into every commit, mirroring the CI lint job.
ci.yml— lint (ruff check + format, mypy), tests (pytest with the 90 % coverage gate) and validation (hassfest+ HACS) via the shared reusable workflowscodeql.yml— GitHub CodeQL security scan; push/PR tomainand a weekly cronrelease.yml— release-please opens a release PR on every push tomainbased on conventional commitsauto-assign.yml— assigns the repository owner to new issues and pull requests
This integration is built and maintained on personal time, on hardware bought for the purpose. If it is useful to you, consider sponsoring the work — it keeps the devices, the testing and the releases coming.