Control and monitor your Lockly smart locks from Home Assistant. This integration communicates with the Lockly cloud API using the same protocol as the official Lockly mobile app.
Unofficial integration. Not affiliated with or endorsed by Lockly Security Inc.
| Feature | Status |
|---|---|
| Unlock from HA | ✅ Verified on PGD628FN + PGH220 hub |
| Lock from HA | 🚧 Implemented; hard to verify on auto-locking locks |
| In-progress state while a command runs | ✅ From 0.7.7 |
| Commands over MQTT when senddata is refused | ✅ Verified on PGD728FN + PGH260 hub (cod=930 accounts) |
| Native Auto-Lock (Automation), set on the lock | ✅ From 0.7.8 on PGK728WRHK; MQTT-only |
| Hubless WiFi-native locks | ✅ Verified from 0.7.4 on two PGK728WRHK (Lockly Visage), firmware 1.14.31 and 3.00.24 |
| Lock state (locked / unlocked) | ✅ At startup and after HA commands |
| Battery level | ✅ Real percentage on WiFi-native locks from 0.7.11; low/normal sentinels elsewhere |
| Door sensor state (if fitted) | ✅ Verified open and closed on a wired sensor; on-demand refresh service from 0.7.10 |
| Last access / who entered | ✅ Read from the lock; names resolve unless a slot is shared |
| Guest PIN management (add / remove / list) | 🚧 In progress |
| Real-time push of external (keypad / app) lock changes | ✅ On WiFi-native locks from 0.7.6 · ⛔ On hub locks — needs an FCM token HA cannot obtain |
| Multiple locks per account | ✅ |
| Silent polling — lock does not beep during polls | |
| Config flow UI | ✅ |
| HACS installable | ✅ |
- Home Assistant 2024.1 or later
- A Lockly account, and a lock that reaches the internet — either behind a PGH-series hub, or a WiFi-native model that connects on its own
- Your Lockly app email and password
Locks that connect only over Bluetooth are not supported. The integration uses the Lockly cloud API, so the lock has to be reachable from it: through a hub, or by its own WiFi. Hubless WiFi-native locks are supported from 0.7.4 and use a different transport — see Known Limitations.
- Open HACS in your Home Assistant instance.
- Go to Integrations → ⋮ → Custom repositories.
- Add
https://github.com/Forcky/LocklyHAwith category Integration. - Search for Lockly and click Download.
- Restart Home Assistant.
- Download the latest release zip file.
- Extract the
custom_components/locklyfolder into your HA configuration'scustom_components/directory:config/ └── custom_components/ └── lockly/ ├── __init__.py ├── api.py ├── config_flow.py ├── const.py ├── lock.py ├── manifest.json ├── mqtt.py ├── sensor.py ├── strings.json └── translations/ └── en.json - Restart Home Assistant.
- Go to Settings → Devices & Services → Add Integration.
- Search for Lockly.
- Enter your Lockly account email and password.
- Click Submit. The integration will log in, discover all locks on your account, and create entities automatically.
Each lock appears as a separate HA device.
Three HA services are available for managing time-limited guest PIN codes. Call them from Developer Tools → Services or from automations.
Status: 🚧 In progress — services are implemented but PIN activation on lock hardware is unverified. See Known Limitations.
| Service | Required fields | Optional fields |
|---|---|---|
lockly.list_guests |
lock_id |
— |
lockly.add_guest |
lock_id, name, passcode (4–8 digits) |
duration_hours (default 24) or start_time + end_time |
lockly.delete_guest |
lock_id, user_acu_id |
— |
Results are returned as HA bus events: lockly_guest_list, lockly_guest_added, lockly_guest_deleted. Listen for these in Developer Tools → Events.
lock_id is the device UUID (visible on the lock's device page in HA under Identifiers).
Quote it in YAML. Plenty of Lockly IDs are all digits —
250021003033471231363531— and Home Assistant's YAML editor has no integer type wide enough to hold one. Unquoted, it is rounded to something like2.5002100303347123e+23before the integration ever sees it, and the original digits cannot be recovered. The service then matches no lock. From 0.7.12 this is reported as an error naming the problem instead of failing silently, but quoting the ID avoids it entirely:action: lockly.refresh_door_state data: lock_id: "250021003033471231363531"
Lockly's Automation mode is executed by the lock itself: the deadbolt stays retracted while the door is open and throws the moment the lock's magnetic sensor detects the door closing. Home Assistant does not poll a door sensor or run a timer to make this happen — it only writes the setting.
| Service | Required fields | Optional fields |
|---|---|---|
lockly.enable_native_auto_lock |
lock_id |
— |
lockly.disable_native_auto_lock |
lock_id |
— |
Results are returned as the bus events lockly_native_auto_lock_enabled and
lockly_native_auto_lock_disabled, each carrying lock_id and success.
Two limits worth knowing. This path is MQTT-only, so a hub-relayed lock will refuse it and log that it could not get a nonce. It is also gated to hardware the behaviour has been verified on — currently PGK728WRHK (type 105) — and other models are declined rather than guessed at.
Enabling while the door is already closed and unlocked does not lock it. Automation acts on the next door-close transition, not on the current state.
The door sensor updates when the lock is commanded, when something asks the lock
directly, and — on some locks — when the door moves. Whether a WiFi-native lock
pushes its magnet state by itself varies by lock, and what decides it is
not yet known:
| Lock | Firmware | Pushes door changes unprompted? |
|---|---|---|
| PGK728WRHK | 1.14.31 | yes, with native Auto-Detection on and off |
| PGK728WRHK | 3.00.24 | no, in a test with every query switched off |
Both were tested with no status queries running. Native Auto-Detection was ruled out as the cause by switching it off on the lock that pushes; it kept pushing. Firmware is the obvious suspect, but one lock on each version can't establish it. On a lock that doesn't push, this service is the only way to learn the door moved; on one that does, it is a way to be certain.
| Service | Required fields |
|---|---|
lockly.refresh_door_state |
lock_id |
The answer lands on the door entity and on a lockly_door_state_refreshed
event carrying lock_id and door_open, so an automation can wait for the
reading it asked for. door_open: null means the lock did not answer, which is
not the same as a closed door.
This wakes the lock. It sends a real Bluetooth status frame, so the lock may chirp — call it when you need a current reading, not on a timer. It is MQTT-only: a hub-relayed lock cannot answer it.
Each lock creates four entities:
- State:
lockedorunlocked, andlocking/unlockingwhile a command is in flight (from 0.7.7) - Services:
lock.lock,lock.unlock - Attributes:
door_sensor_open— door circuit state, if a sensor is fitted.truealso occurs on locks with no sensor; see Door sensorfirmware_version— lock firmware string (available from live query)auto_unlock_delay_s— configured auto-lock delay in seconds (available from live query)
How long
locking/unlockinglasts depends on your hardware. It covers the whole command, so on a hub-attached lock it is roughly onesenddataround trip. On a hubless WiFi-native lock it spans two transports —senddatabeing refused, then the command going over the broker — so it is visibly longer. That is the entity reporting honestly how long the command takes, not a stall. It clears even if the command fails.
- State: a percentage, from the best source available for that lock
- Device class:
battery - Attributes:
source— which of the three sources produced the readingvoltage— the wakeup voltage, when a live query has supplied onelow_battery— raw boolean from the cloud or live query
There are three sources, and the sensor prefers them in this order:
source |
Where it comes from | How much to trust it |
|---|---|---|
reported |
the percentage the lock pushes on the MQTT channel, or, for a lock on a rechargeable pack, the one in its status response | the lock's own figure, the same one the Lockly app shows |
voltage |
wakeup_voltage from a live query, through our 4×AA curve |
a real measurement, but our curve rather than Lockly's. AA locks only: a rechargeable pack reports no voltage (from 0.7.16) |
low battery flag |
the cloud cache's binary low/normal flag | not a measurement — 10 % and 90 % are sentinels standing in for "low" and "not low" |
Check
sourcebefore you set a low-battery alert. A lock that only ever reports the binary flag reads 90 % when it is fine and 10 % when it is not, and will never show you anything in between. Locks that push a real percentage (WiFi-native models, from 0.7.11) do not have this problem.
- State:
openorclosed - Device class:
door - Availability:
unavailableuntil the lock has reported a closed door at least once
The availability rule is not arbitrary. The lock reports a door circuit, not a door sensor: a shut door completes the circuit, an open door breaks it — and a lock with no sensor fitted is a broken circuit permanently. So "open" is ambiguous between a genuinely open door and no sensor at all, while "closed" can only come from a real sensor. The entity therefore appears the first time a lock reports closed, and stays available from then on.
In practice it appears on the first poll for any lock whose door is shut. From
0.7.17 a closed reading pushed over MQTT counts too, so a lock that reports its
door by push, or carries magnet=closed in its lock and unlock callbacks, shows
the entity as soon as it next reports. If it stays unavailable, shut the door
and trigger a refresh — restart HA, or send any lock command. The learned state
is held in memory, so it is re-learned after each restart. See
Known Limitations.
When the integration loads, it authenticates with the Lockly cloud and retrieves the full lock list. It then attempts one silent cloud-cache poll per lock. If that fails (e.g. hub firmware is too old), it sends one live BLE query per lock to get the initial state. This may cause a brief one-time beep on each lock at startup.
The integration polls the Lockly cloud cache endpoint (lock/cachedstatus/get) every 30 seconds. This endpoint returns the last known state the hub uploaded to the cloud — no BLE command is ever sent to the physical lock during polling, so the lock does not beep.
Silent polling requires hub firmware with a sufficiently recent build number:
| Hub major version | Minimum build |
|---|---|
| 2.x | 422 (e.g. 2.2.04.22) |
| 4.x | 401 |
| 6.x | 503 |
If the hub firmware is older than these minimums, the server returns an error and the integration switches to "no-poll" mode: state is preserved from the last successful query and only updates when a lock/unlock command is sent through HA. No periodic BLE commands are ever sent — the lock will not beep on a timer regardless of hub firmware version.
To check your hub firmware, look in the Lockly app under Hub settings, or check the HA logs for a line like:
Lockly: cachedstatus unsupported for this hub (hub firmware too old) — state will only update after HA commands
When you lock or unlock from HA, the integration sends a BLE command through the Lockly cloud and hub to the physical lock. The lock will beep once to acknowledge the command. The lock state in HA updates immediately (optimistically) without waiting for the next poll cycle.
The integration polls getlkhist every 5 minutes for each lock and fires a lockly_lock_event HA bus event for each new entry. The event includes event_type, user_name, timestamp, and event_id. The Last Access sensor on each lock device shows the most recent entry's user name and event details.
Status: 🚧 In progress — event field names have been verified against the APK, but live hardware testing is still required.
The integration connects to the Lockly MQTT broker at startup and listens on its own client topic for state callbacks. Whether anything arrives depends on your hub being connected to that channel.
On the hardware this was developed against, a PGH220 on firmware build 417, the
broker answers commands with 3005 device is offline — the hub is not on the
channel, so no push arrives and state behaves as described above. Newer hubs
appear to be connected to it, since the official app uses this channel to
control locks over WiFi.
Earlier versions declared this impossible. That was wrong: the integration had
been subscribing to server, which is a publish-only topic that no client reads
from, and the broker was correctly refusing it. Replies and callbacks arrive on
client/<client_id> instead.
The broker grants this subscription, so the channel itself is open:
Lockly MQTT connected, subscribing to 'client/…'
Lockly MQTT subscribed to 'client/…' at qos=[0]
That is verified working, including on the build-417 hub. But listening is not the same as being sent external state, and two flows share this channel — only one of which reaches Home Assistant:
-
Command confirmation works. The fresh state that comes back right after an HA-initiated lock or unlock is routed to our own
client/<client_id>topic. Verified on aPGD728FNbehind aPGH260hub: lock state is correct immediately after every command. -
Unsolicited push depends on the lock, and 0.7.6 fixed our half of it. On a hub-attached lock it still does not arrive: the server pushes external changes only to a client id registered through Lockly's Firebase/AIPN service, which needs a real FCM token from a genuine app install (
JobService.u()in the decompiled app), and Home Assistant's ephemeral client id is never registered.WiFi-native locks are different — they send
deviceStateCallbackmessages to our own client topic without any of that, and until 0.7.6 this integration discarded every one of them. It looked for the item list at the root of the message, where it is actually underpayload, and matched the uppercaseLOCKED_STATUSkey, where a Visage sends lowercaselockwith the valuelockedorunlocked. Both shapes came from reading the app and neither had ever been checked against a live message. On these locks a keypad or app unlock now reaches Home Assistant.A
magnetkey does exist, carryingopenedorclosed, and 0.7.11 reads both — until then onlyclosedwas understood, so a door sensor could reach closed and never leave it. Whether a lock sends it unprompted varies: one PGK728WRHK on firmware 1.14.31 does, with native Auto-Detection on or off, and one on 3.00.24 produced no callback at all with every refresh disabled. What decides it is not established — see Refresh door state on demand. Captured on #10.
So state is accurate immediately after you act through HA, and otherwise stale
until the next HA command — or the next successful poll, on hubs new enough for
silent polling. A SUBACK 0x80 refusal on the client topic is not fatal, since
the broker was observed delivering a message without granting a subscription, so
the connection is kept regardless.
Credentials (email and password) are stored in HA's config entry. The integration obtains a JWT at startup and automatically re-authenticates when it expires.
-
Bluetooth-only locks are not supported. The integration talks to the Lockly cloud, so a lock it can never reach is out of scope. WiFi-native locks with no hub are supported — see below.
-
Hubless WiFi-native locks: commands work,
cod=930in the log is expected. Models such as thePGK728WRHKandPGD728FG25connect straight to WiFi with no hub to relay through, so theirhubidis empty and thesenddataendpoint refuses everything they send withcod=930. That is by design and not a fault: commands for these locks go over the MQTT transport instead, and from 0.7.4 they work. Verified on twoPGK728WRHK(Lockly Visage) on firmware 1.14.31 and 3.00.24 — lock and unlock both succeed and the state change is confirmed back over the broker. From 0.7.17 their state is also read over the broker when Home Assistant starts, so they no longer sit atunknownuntil someone uses them, and the integration no longer sends themsenddataorcachedstatuscalls that can only fail.What still does not work on them:
senddata's state and access-log queries, for the same reason, andQueryPwd147(0x93) returns0xFA, so the host credential is read from the cloud's copy rather than from the lock. Neither blocks locking or unlocking. -
Unlock is verified from 0.5.0. Two fields in the command frame were wrong: the
str3hub flag was sent as00instead of01, and the credential slot was sent as1instead of0(the host credential lives in slot 0). Both had to be right at once, which is why this took so long to find. Confirmed against physical hardware — a PGD628FN on firmware 4.03.15 behind a PGH220 hub. -
Commands fall back to MQTT when
senddatafails, and this is verified. If your logs are full ofcod=930while the official Lockly app controls your locks normally, the app is reaching them over Lockly's MQTT broker rather than throughsenddata. The integration now does the same: when a lock or unlock is refused, it fetches a fresh nonce, reads the host password from the lock, and sends the command over the broker, all on that transport.Confirmed working from 0.7.1 by a user whose
senddatacalls all returncod=930— locking and unlocking both succeed. That account is on aPGD728FNbehind aPGH260hub.Reading the host password from the lock is not yet modelled for every lock type — a
PGD728FNcredential frame has a layout this parser does not fully decode, so on that model the command is built from the cloud's copy of the password instead, which works. The earlier releases logged a traceback here; that is fixed and the fallback is silent.Note what is not yet routed this way: the periodic status query and the access log still go through
senddata, so on an affected account lock state can be stale even while commands work. That is the next thing to move across. -
WiFi-native locks: the command frame was corrected in 0.7.4, and is confirmed. Locks with no hub at all reached the broker and were then refused by the lock itself with
0xFF, "wrong password". The credential was never the problem — the frame was. The0x52command these locks use carries a two-byte access-user field where the integration sent one, ends in the phone's clock as an 8-byte value rather than the lock's stored nonce, and is wrapped with encryption type0xBrather than5. The first of those left every later field a byte out of place, which is enough on its own to fail a credential check that would otherwise pass. All three shipped together; sending one without the others only moves the corruption.Diagnosed from
NewUnlockCmd.getDatarather than from a capture, by the reporter of issue #3, and confirmed by them on two locks within hours of the release. -
Real-time push of external changes: depends on the lock, and this was wrong until 0.7.6. State arriving right after an HA-initiated lock or unlock has always been correct — that reply is routed to our own MQTT client topic. Beyond that, earlier versions said flatly that outside changes could never reach Home Assistant, on the grounds that the server only pushes to a client id registered through Lockly's Firebase/AIPN service, which needs an FCM token from a genuine app install. The registration part is still true, and it is still why hub-attached locks do not push.
But WiFi-native locks do send
deviceStateCallbackmessages to our own client topic, and this integration was throwing them away: it looked for the item list at the root of the message where it actually sits underpayload, and matched an uppercaseLOCKED_STATUSkey where a Visage sends a lowercaselockwith the valuelockedorunlocked. Both came from reading the app rather than a capture, and neither had been checked against a live message. 0.7.6 reads both shapes, so on these locks an unlock at the keypad or in the Lockly app now reaches Home Assistant.Door state depends on the lock. Every WiFi-native lock answers a status query with an accurate
magnetreading, but only some push the change by themselves: a PGK728WRHK on firmware 1.14.31 does, whether native Auto-Detection is on or off, and one on 3.00.24 produced no callback in either direction with every refresh disabled. On a lock that doesn't push, the door sensor updates when something asks, not when the door moves — which is whatlockly.refresh_door_stateis for. Firmware is the leading suspect for the difference but is not established. Tested on two PGK728WRHK and reported on #10. -
Silent polling requires hub firmware build ≥ 422 (for major-version-2 hubs). On older firmware
lock/cachedstatus/getreturnscod=900and state only updates at startup and after HA commands. Note that Lockly does not necessarily offer an upgrade: a PGH220 on2.2.04.17(build 417) reports itself up to date, five builds short of the requirement. -
Door sensor state is verified, but sensor presence cannot be read from the lock. Bit 0 of the status byte is the door circuit:
0= closed,1= open. A closed door completes the circuit and an open door breaks it — but a lock with no sensor fitted is an open circuit permanently, so it reads1too. That makes1ambiguous between "door open" and "no sensor fitted", and this ACK carries no separate presence flag. (The hub'slock/cachedstatus/getresponse does have one, at bit 1, but that endpoint needs newer hub firmware — see above.)The integration resolves the ambiguity by observation instead. A
0can only come from a real sensor, so the first time a lock reports a closed door itsDoorentity becomes available and stays available. A lock that has never reported closed keeps the entityunavailable— the honest answer, rather than showing a permanently-open door that may not exist. In practice the entity appears on the first poll for any lock whose door is shut. If it staysunavailable, shut the door and trigger a refresh (restart HA, or send any lock command). One closed reading is enough, though it is held in memory only and re-learned after each restart.Confirmed by physically opening a sensor-equipped door and watching the bit flip. Two earlier readings of this bit were wrong — first as "sensor connected", then as its inverse — and each fitted every sample available at the time, because those samples all happened to have the sensor-equipped doors shut. Four locks agreeing is not evidence when all four share a confound.
-
Battery percentage is approximate (binary low/normal flag only).
-
Guest PIN activation is unverified:
lockly.add_guestcreates the guest record on the Lockly cloud. Whether the PIN is automatically pushed to the lock hardware is not yet confirmed. If the PIN does not work physically, open an issue. -
lock.lockis implemented but unverified. It differs from unlock by a single byte, and the app confirms these locks support an explicit lock command (isSupportNewLockcovers PGD628FN). It is genuinely hard to observe on a lock withautoLockset, since the bolt throws itself within seconds either way. Please report whether it works on your model. -
The Lockly API is undocumented and reverse-engineered. A firmware update by Lockly could break this integration. See
docs/api.mdfor the full protocol documentation. -
The RSA keys embedded in
api.pyare extracted from Lockly app version 3.2.9. If Lockly rotates the keys in a later app release, the integration will stop working until updated.
Integration shows "cannot_connect"
- Check that your Lockly hub is online in the official app.
- Verify your HA instance has outbound HTTPS access to
apiserv03c.lockly.com.
Integration shows "invalid_auth"
- Ensure you're using your Lockly app email and password (not a Google/Apple SSO account).
- Try logging out and back in to the Lockly app to verify credentials.
Locks show as unavailable after setup
- The cloud cache may take up to 60 seconds to warm up after a hub reconnect. Wait and then reload the integration.
Lock state does not update when physically used
- This is expected when your hub firmware is below the silent-polling minimum. State updates only when commanded through HA or at the next restart.
- To get background state sync, update your hub firmware via the Lockly app to version
2.x.04.22or later (for PGH220-series hubs).
Locks beep every 30 seconds
- This should not happen with the current version. If it does, check you are running the latest release and reload the integration.
Unlock works but lock does not close the door
- Some older Lockly hardware generations may use a different BLE command for locking. Open an issue with your lock's BLE name (shown in the Lockly app) so support can be added.
Last Access sensor shows "Unknown"
- Normal for anonymous keypad entries (no named user assigned to that PIN in the Lockly app). The sensor will show a name once a named user is added in the app.
lockly_lock_event bus events never fire
- Access log polling runs every 5 minutes. Wait at least 5 minutes after manually operating the lock.
- Check HA logs for
getlkhist failed: cod=lines with debug logging enabled.
MQTT shows "connection refused" in logs
rc=5("not authorised") has so far followed the login token rather than the account: on the account this was developed against, a fresh login cleared every one. From 0.7.15 the integration does that itself. Once every broker address has refused, it logs in again and retries after 30 seconds, then 5 minutes, and so on up to every 6 hours, and the warning says when the next try is. Polling carries on in the meantime. It also renews the login token an hour before the token expires, so a reconnect is never offered a stale one.- Recovery logs
Lockly MQTT connected again after a fresh loginat INFO, so you needcustom_components.lockly: infoordebugunderlogger:to see it. If refusals keep coming for hours, please open an issue with the log.
lockly.add_guest succeeds but PIN does not work on the lock
- The guest record was created on the server but the PIN may not have been pushed to the lock hardware. Open an issue with your lock model so the passcode activation step can be implemented.
Enabling debug logs
Add this to your configuration.yaml:
logger:
default: warning
logs:
custom_components.lockly: debugPull requests welcome. Please:
- Open an issue first for significant changes.
- Include your lock model and hub model if reporting a device-specific bug.
- Read
AGENTS.mdfor architecture notes and critical invariants before writing code. - See
docs/api.mdfor the full API protocol — useful background if you're extending the integration.
MIT — see LICENSE.
This integration is not affiliated with, endorsed by, or supported by Lockly Security Inc. Use at your own risk. The integration was developed by reverse-engineering the Lockly Android app (version 3.2.9) for personal home-automation purposes.