Skip to content

Releases: Forcky/LocklyHA

v0.7.12 — the ID that was silently a number

Choose a tag to compare

@Forcky Forcky released this 28 Sep 18:42

A failure that told you nothing, and a documented quirk of the hardware.

An all-digit lock ID has to be quoted

Many Lockly device IDs are entirely digits — 250021003033471231363531. Home Assistant's YAML editor parses that in JavaScript, which has no integer type wide enough to hold it, so an unquoted ID above 2⁵³ arrives as 2.5002100303347123e+23. The digits are gone before this integration runs, and nothing can recover them.

Every service takes a lock_id, so this affected all of them, and the symptom was silence: no lock matched, refresh_door_state logged door_open=None, and the owner was told neither what was wrong nor that anything was.

It is now reported as an error naming the mistake and the fix. Quoting avoids it entirely:

action: lockly.refresh_door_state
data:
  lock_id: "250021003033471231363531"

Every lock_id field description says so now, and so does the README. Found by @Dei381rcr while testing 0.7.11 — the kind of thing only real use turns up.

The Lockly app's Auto-Detection toggle can lie

Documented in docs/api.md, in the reporter's words and with his permission:

On my PGK728WRHK hardware, a settings value of 0x0A corresponded with the lock performing its native automatic locking behavior even when the Lockly app showed Auto-Detection as off. Changing the settings value from 0x0A to 0x02 stopped that native automatic locking behavior.

That is bit 0x08 — the Auto-Lock master switch — set in the lock's own settings byte while the app displayed the opposite. The lock was enforcing what its byte said.

This matters if a lock is throwing its bolt at an open door: the app's switch is not evidence, and lockly.disable_native_auto_lock is how to be sure, because it reads the byte, clears the bit and reads it back. It is also why that write has always been a read-modify-write rather than a blind set. Scoped to the PGK728WRHK hardware it was observed on, as the reporter asked — nothing here establishes how other models behave.

Also settled: a status query wakes the lock

Tested on a Visage — the lock audibly chirps when queried. So no automatic door-state polling will be added. lockly.refresh_door_state stays a service you call when you need a reading, which is what 0.7.10 shipped it as.

Unchanged

No frame, transport or state-parsing changes. v0.7.11's magnet and battery fixes are confirmed working on two PGK728WRHK locks, both now reporting a real percentage with source: reported.

v0.7.11 — the door sensor can open again, and the battery is a real number

Choose a tag to compare

@Forcky Forcky released this 28 Sep 02:17

Two fixes and a documentation correction, all out of @Dei381rcr's testing on a Lockly Visage.

The door sensor could close but never open

A WiFi-native lock sends magnet: opened and magnet: closed. This integration accepted open. So every door closing was recorded and every door opening was dropped — on the affected hardware the door entity could reach closed and stay there.

It was reported as missed callbacks, which is exactly what it looked like from the outside. The warning about an unrecognised state value had been sitting in the debug log the whole time; nobody went and read it, me least of all.

Both word forms are accepted now, and close/closed alongside them.

The lock does not push door state — it answers when asked

Worth stating plainly, because two places in the README said the opposite in each direction.

With every refresh automation disabled and no status queries running, opening and closing a door by hand produced no callback at all, in either direction. The same lock answers a status query with the correct magnet state every time.

So the door sensor updates when something asks, not when the door moves, and lockly.refresh_door_state from 0.7.10 is the mechanism rather than a workaround. The README now says so.

Battery is a real percentage where the lock sends one

The same callback carries a numeric battery level — seen on a PGD728FG25 and two PGK728WRHK. The sensor now prefers it over everything else, and a new source attribute says where the current reading came from:

source Meaning
reported the lock's own percentage, as the Lockly app shows it
voltage a live wakeup-voltage reading through our 4×AA curve
low battery flag not a measurement — 10 % and 90 % standing in for "low" and "not low"

Check that attribute before setting a low-battery alert. A lock that only reports the binary flag will show 90 % until the day it shows 10 %, and nothing in between. That has always been true; until now nothing on the entity said so.

A value outside 0–100 is refused rather than clamped — a lock reporting 255 is saying something other than "full".

Unchanged

No frame or transport changes. Hub-attached locks behave exactly as in 0.7.10, including the battery sentinels, which are still all their cloud data supports.

v0.7.10 — ask the lock what the door is doing

Choose a tag to compare

@Forcky Forcky released this 23 Sep 11:06

Adds lockly.refresh_door_state, contributed by @Dei381rcr.

Why it exists

The door sensor updates when the lock is commanded and, on WiFi-native locks, when the lock pushes a state callback. Neither covers a door that opens and closes without the lock being touched — and on a Lockly Visage no magnet state appears to be pushed at all, so a door could open with Home Assistant none the wiser. Reported with reproductions on #10.

The service asks the lock directly and publishes what it answers. The reading lands on the door entity and on a lockly_door_state_refreshed event carrying lock_id and door_open, so an automation can act on the answer it asked for. A door_open of null means the lock did not answer — which is deliberately not the same as a closed door.

It wakes the lock. This 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.

Also in this release

The repository now runs CI on every push and pull request — ruff, plus both test suites. The tests need no network, broker or hardware, so there was never a good reason they only ran on one machine.

The lint rules are deliberately narrow: pycodestyle and pyflakes, nothing about taste. It exists for a specific reason. Three pull requests in a week carried trailing whitespace on a blank line, one of them a PR whose only purpose was removing trailing whitespace, and it survived three attempts — because github.com renders a whitespace-only line identically to an empty one and no reviewer can see the difference. A machine can.

Unchanged

No frame, transport or state-parsing changes.

v0.7.9 — debug logs that fit the message

Choose a tag to compare

@Forcky Forcky released this 22 Sep 18:46

A small fix with an outsized effect on how this project gets its information.

The raw MQTT debug line was trimmed at 400 characters — shorter than the messages it exists to show. The reporter on #2 sent a log to answer a question about which state keys a deviceStateCallback carries, and the state keys were exactly the part that had been cut off. Every protocol question this project has answered was answered from one of these logs.

The limit is now 2000 characters, which keeps a callback (~600) and a command reply with its base64 frame (~900) whole. Anything genuinely longer is still trimmed, but now says how many characters it dropped instead of ending mid-word and looking like the message itself was malformed.

If you have been asked for a debug log, this is the version to capture it on.

Unchanged

Nothing else. No frame, transport, state or service changes.

v0.7.8 — the lock locks itself when the door shuts

Choose a tag to compare

@Forcky Forcky released this 22 Sep 01:08

Adds Lockly's native Auto-Lock (Automation) mode, contributed by @Dei381rcr. The deadbolt stays retracted while the door is open and throws the moment the lock's own magnetic sensor sees the door close.

The important part is where this runs. The lock does it, not Home Assistant. HA writes the setting once and then stays out of the way — no door-sensor polling, no timer, and it keeps working while HA is down or the network is out.

Using it

Two new services, both taking lock_id:

  • lockly.enable_native_auto_lock
  • lockly.disable_native_auto_lock

Each fires a bus event (lockly_native_auto_lock_enabled / ..._disabled) carrying lock_id and success.

Three things to know before you try it

It is MQTT-only. A hub-relayed lock will decline and log that it could not get a nonce. If your lock works through a hub, this release changes nothing for you.

It is gated to verified hardware — currently PGK728WRHK (type 105), tested on firmware 3.00.24 and 1.14.31. Other models are refused rather than guessed at. This is the same posture the firmware-gated 0x52 models already get: a command built on an unverified frame layout is worse than no command.

Enabling it while the door is already shut and unlocked does not lock the door. Automation acts on the next door-close transition, not on the current state. This looked like a failure during testing and is not one.

Protocol

Both commands were verified against the decompiled app rather than inferred from behaviour:

  • 0x12 carries auto-lock time as an unsigned little-endian 16-bit field — 0100 selects Auto-Detection, 0000 disables. A one-byte time field happens to produce the right value for Automation and shifts every field after it, which is the kind of bug that passes a hardware test and fails everything else.
  • The check-door-sensor byte that follows exists only on models whose capability predicate includes it, and is omitted entirely otherwise.
  • 0x19 uses 0x80 as its query sentinel; bit 3 is the Auto-Lock master switch, and a write serializes only the low nibble, forcing bits 4-7 to zero.

Documented in docs/api.md, with regression vectors in tests/test_frame.py.

Unchanged

No changes to lock, unlock, status parsing, the access log or the MQTT transport. Everything from 0.7.7 behaves identically.

v0.7.7 — the lock shows what it is doing

Choose a tag to compare

@Forcky Forcky released this 17 Sep 18:22

The lock entity now reports locking and unlocking while a command is in flight, instead of sitting on its old state until the command finishes. Contributed by @djbn65.

How long the in-progress state lasts

Please read this before reporting a stall. The state covers the whole command, so how long it shows depends on your hardware:

  • Hub-attached lock — roughly one senddata round trip.
  • Hubless WiFi-native lock — noticeably longer, because a command there spans two transports: senddata is refused with cod=930, as it always is on these locks, and only then does the command go over the broker.

So a Visage or a PGD728FG25 will show unlocking for longer than a hub lock does, and that is the entity reporting honestly how long the command actually takes rather than anything being stuck.

It also clears if the command fails, rather than leaving the entity in-progress until Home Assistant restarts — verified on both paths.

Unchanged

No frame, transport or state-parsing changes. Everything from 0.7.6 behaves identically.

v0.7.6 — external lock changes now reach Home Assistant on WiFi-native locks

Choose a tag to compare

@Forcky Forcky released this 17 Sep 04:39

Two fixes, both found by @Dei381rcr testing on hardware this project does not own. Also worth recording: the 0.7.4 frame fix is now confirmed on a third lock and a second model, a PGD728FG25 on issue #2.

External lock changes now arrive

Every version until now said real-time push was impossible without a Firebase token. That is still true of hub-attached locks. It was never true of WiFi-native ones — they push deviceStateCallback messages to our own client topic with no registration at all, and this integration was throwing every one of them away:

  • it looked for the item list at the root of the message, where it actually sits under payload — the same place every other message this client handles keeps it
  • it matched an uppercase LOCKED_STATUS key with the value "1"/"0", where a Visage sends a lowercase lock with the value locked/unlocked

Both readings came from the decompiled app, and neither had ever been checked against a live message. Both shapes are now accepted, so on a WiFi-native lock an unlock at the keypad or in the Lockly app reaches Home Assistant.

A state value in a form this does not recognise is now logged as a warning asking you to report it, rather than quietly being treated as False. Guessing wrong about a lock or a door is worse than saying so.

Door state is not covered by this. No magnet key has appeared in any captured callback from these locks, so the door sensor still only updates from a status query.

Status readings over MQTT are no longer discarded

_mqtt_nonce took the nonce and the lock type out of a status response and dropped the rest. On an account where senddata is refused, MQTT is the only transport, so the lock and door state parsed from every status query went in the bin. A door entity could sit stale, or stay unavailable for the life of the run, because nothing else ever proved its sensor was fitted.

It now publishes through the same path the senddata status query uses, extracted so the two cannot drift apart again, and that path fills in the sensor-proven flag the door entity's availability depends on. Verified by the reporter: a stale "open" corrected to closed, and an unavailable sensor came to life.

Unchanged

No frame or transport changes. Hub-attached locks behave exactly as in 0.7.5 — they still do not receive external state, for the Firebase reason, which has not changed.

v0.7.5 — log cleanup for WiFi-native locks

Choose a tag to compare

@Forcky Forcky released this 15 Sep 04:22

v0.7.4 is confirmed working on hubless WiFi-native locks — two PGK728WRHK (Lockly Visage) on firmware 1.14.31 and 3.00.24, both locking and unlocking over the broker, reported within hours of the release by the user who diagnosed the frame.

This release fixes the logging, which was still describing those locks as unsupported and every one of their commands as a failure.

Fixes

  • An empty hubid is no longer a warning. It means the lock is WiFi-native and reaches the cloud on its own. mc and hc still warn, because those genuinely break commands.
  • "WiFi-native locks are not yet supported; see issue #2" is gone. It was logged at ERROR once per command, on locks that were working. It is now a debug line saying the command is going over MQTT instead.
  • "failed — see the senddata warning above" no longer precedes a successful unlock. It was logged before the MQTT fallback had even been attempted, so on a hubless account it sat above every command that then succeeded. The attempt is now an info line, and the warning appears only if both transports fail.
  • cod=930 reads as expected rather than fatal on a lock with no hub.

Docs

The README no longer says locks without a hub are unsupported. It records that hubless WiFi-native locks work from 0.7.4, on which hardware that was verified, and what is still missing on them: senddata's state and access-log queries are refused for the same reason commands were, and QueryPwd147 (0x93) answers 0xFA, so the host credential comes from the cloud's copy rather than from the lock. Neither blocks locking or unlocking.

docs/api.md records the 0x52 layout as confirmed on hardware rather than derived from source alone.

Unchanged

No frame or transport changes. If 0.7.4 works for you, 0.7.5 behaves identically and says less about it.

v0.7.4 — the 0x52 command frame, corrected

Choose a tag to compare

@Forcky Forcky released this 14 Sep 20:36

A fix for hubless WiFi-native locks, from a source-level diagnosis rather than a capture.

Confirmed working. Verified on two PGK728WRHK (Lockly Visage) on firmware 1.14.31 and 3.00.24 — lock and unlock both succeed over the broker, with the state change coming back on the client topic. Reported on issue #3 within hours of release. This release shipped as untested; it is not any more.

Take v0.7.5 instead: same frame, without the log messages that still called these locks unsupported.

What was wrong

Locks with no hub reach the lock over the MQTT broker and are then refused by the lock itself with 0xFF, "wrong password". That looked like a credential problem. It was not: the command frame was malformed, and the lock was reading the credential fields at the wrong offsets.

NewUnlockCmd's isSupport82Cmd branch — the 0x52 command every WiFi-native model uses — differs from the ordinary 0x22 layout in three places, and this integration built none of them:

  • The field after the password is two bytes, not one. getUserId() runs both of its branches through getCmdLenString, which is little-endian 16-bit. For the lock's owner the value is 0: isOwner() is defined as userId == 0 && adminId == 0, and nothing in the app gives a host a non-zero user ID — the cloud restore path copies sixty-odd fields out of the device record and never that one. A byte short here left the action, hub flag and trailing field each read a byte early, which is enough on its own to fail a credential check that would otherwise pass.
  • The trailing field is the phone's clock, not the lock's nonce. isSupportTimestamp() is true for all of these models, so the app sends DataUtils.p(System.currentTimeMillis()) — eight bytes little-endian — and never replays the stored value.
  • The frame's encryption type is 0xB, not 5. The 0x52 branch overrides getEncryptType() with isHost() ? 11 : isLongTerm() ? 13 : 12.

All three ship together. Two of them change the frame's length, so correcting one without the others only moves the corruption.

Credit

Diagnosed by @Dei381rcr against Lockly Android 3.3.4, including the getCmdLenString and encryptType findings and the observation that 0xFF was not proof the credential was wrong. Confirmed here against app 3.2.9 and Lockly Home 1.4.8 before shipping, along with the missing piece — where BluetoothBean.userId comes from for a host.

How to help

If you have a PGK728WRHK, PGD728FG25 or another hubless Lockly, update and try lock and unlock once each with debug logging on, then paste the log on #2 or #3:

logger:
  default: warning
  logs:
    custom_components.lockly: debug

Either it works, or the error byte tells us which field is still wrong. Both outcomes are useful.

Unchanged

Hub-attached locks are untouched: the 0x22 frame is byte-for-byte identical to 0.7.3, and the status query and QueryPwd147 keep encryption type 5 — QueryPwd147Cmd asks getTenantAccessEncryptType(), which falls through to getEncryptType() for a host, so the 0xFA those locks return to 0x93 is still unexplained.

Vision locks now send encryption type 0xB on the 0x22 path as well, which is what (isVision() && isHost()) does in the app. No Vision hardware has been tested against this integration, so if a Vision lock stopped working with this release, please say so on an issue.

v0.7.3 — MQTT capability ordering, parse crash fix, honest push docs

Choose a tag to compare

@Forcky Forcky released this 12 Sep 04:04

A bug-fix release, driven almost entirely by two users testing WiFi-native locks. Thank you both.

Fixes

Capabilities were read too early on the MQTT command path. When senddata is refused and the integration falls back to the broker, it now reads the lock's capabilities after the status query rather than before. That query is where the real lock type is learned, and it can differ from the guess made off the lock list. Reading it too early meant the first command could be built with the wrong command code: a type-105 lock that needs 0x52 was getting the default 0x22. Found and diagnosed by the reporter of issue #3 on two hubless Lockly Visage locks.

A credential frame from a PGD728FN no longer crashes the parser. Its layout is not one this parser fully models, and the field walk ran off the end and raised a ValueError in the log. It now stops cleanly and falls back to the cloud copy of the password, which is what already happened, just without the traceback.

Docs: real-time push, stated accurately

Earlier notes implied real-time push might work on a suitable hub. That was too broad. Two things share the MQTT connection and only one reaches Home Assistant:

  • State that comes back right after an HA lock or unlock is correct. That reply is routed to our own client topic and is verified.
  • A change made at the keypad, in the Lockly app, or over Matter is not reflected in HA. The server only pushes those to a client registered through Lockly's Firebase notification service, which needs a token a third-party client cannot get.

So state is accurate after you act through Home Assistant, and otherwise updates on the next command or successful poll. This is now documented plainly in the README and docs/api.md.

Where WiFi-native locks stand

Both hubless models under test (PGD728FG25 and PGK728WRHK) now reach their locks over the broker and are rejected with 0xFF (wrong password) using the cloud credential. The transport and frame appear correct; the credential value is the open problem. Tracked in issues #2 and #3.

Unchanged

Silent polling still needs hub firmware build 422 or newer, and signal readings are only available on PGH260 (Matter) hubs.