Feature/dlklap transport - #1729
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1729 +/- ##
==========================================
+ Coverage 93.29% 93.39% +0.10%
==========================================
Files 157 159 +2
Lines 9932 10222 +290
Branches 1022 1052 +30
==========================================
+ Hits 9266 9547 +281
- Misses 471 475 +4
- Partials 195 200 +5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
|
||
|
|
||
| def _sha256(payload: bytes) -> bytes: | ||
| return hashlib.sha256(payload).digest() # noqa: S324 |
There was a problem hiding this comment.
CodeQL False Positive: SHA256 is required by the DLKLAP handshake (auth-hash derivation), not password storage. Mirrors the existing _sha256 in klaptransport.py, which carries the same # noqa: S324. Safe to dismiss.
|
Thanks for this work @tedholtz Hope it gets committed so I can test it out! |
|
I have 2 locks, a DL105 and a DL110. I was able to get them both working with this by adding the models in device_fixtures and changing _ensure_device_id to look up the MAC address for the IP and match on that: |
@rale thanks for testing. |
Summary
Adds first-class support for the TP-Link DL100 smart lock via a new
DlklapTransportand aLockSmartModule. Closes #1693.The DL100 speaks DLKLAP, a proprietary KLAP variant that runs over plain
HTTP :80 and requires a cloud-assisted
handshake0step to mint aper-session
controlKeybefore the usual KLAP handshake1/handshake2 andAES-128-CBC session encryption. This transport implements that full pipeline
and slots into the existing Transport / Protocol / Device layering.
What is DLKLAP?
Standard KLAP handshakes are fully local. DLKLAP inserts two cloud round-trips:
wap.tplinkcloud.com) → account token +accountId.handshake0to the lock over HTTP → a 236-char base64secret(thisalso wakes the lock's radio).
*.tplinknbu.com) binds thatsecretto aper-session
controlKey.keys (
lsk/ldk/iv) used for/app/request.Full protocol write-up is in #1693.
Changes
kasa/transports/dlklaptransport.py—DlklapTransport(BaseTransport)_DlklapSession. Implementshandshake0 → control-key → handshake1 → handshake2 → session-key derivation,session caching, and single-retry re-handshake on failure. Handshakes are
serialized (
asyncio.Lock) since a secondhandshake0invalidates the firstcontrol key (device error
15033).kasa/smart/modules/lock.py—Lock(SmartModule):is_locked,lock(),unlock(),battery,battery_low.DeviceEncryptionType.Dlklap = "DLKLAP",DeviceFamily.SmartTapoLock = "SMART.TAPOLOCK",DeviceType.Lock, theSMART.DLKLAP → (SmartProtocol, DlklapTransport)mapping indevice_factory.py, and theSMART.TAPOLOCK → DeviceType.Lockmapping insmartdevice.py.tests/smart/modules/test_lock.py(mocked, no live device) plus aredacted DL100 fixture folded into the parametrized SMART suite.
pyproject.toml— addshttpxto dependencies.Behavior notes for reviewers
httpxused directly (notHttpClient) in the transport. This isdeliberate: the 33-byte binary
handshake0body must be sent verbatim, andwrapping middleware re-encodes the
text/plainpayload and breaks thehandshake. Happy to revisit if there's a preferred integration point.
verify=Falseis scoped to a single call. The control-key host presentsTP-Link's private CA (not a public root), so TLS verification is disabled for
that one request only (
# noqa: S501with a comment). The cloud logincall, which carries the account password, stays fully verified.
Lockmodule because the DL100 lacks thebattery_detectcomponent, so the genericBatterySensornever loads.0 = LOCKED(bolt extended),
1 = UNLOCKED. Confirmed against decompiledEnumDoorLockStatusand the live device — not inverted.Testing
pytest tests/smart/modules/test_lock.py— all pass(module presence,
is_lockedpolarity,lock()/unlock()payload withowner-forbidden fields excluded, battery).
pytest tests/— 22,856 passed, 883 skipped, 0 failures,including the
test_devtools.pyfixture round-trip check.mypyclean on the new files;ruff/ruff formatclean.1.0.17) viadevice_factory.connect()→update()—is_locked=True(lock_status 0),battery_level=81,battery_low=False.Known limitations / follow-ups
_ensure_device_idcan't disambiguate multiple locks on one account (thecloud device list has no LAN IP), so it picks the sole
SMART.TAPOLOCK.A future config override could address multi-lock setups.
speakercomponent it doesn't implement(
getVolume → UNKNOWN_METHOD_ERROR); handled gracefully (module markedunavailable). Cosmetic device quirk, not a bug.