FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Feature/dlklap transport by tedholtz · Pull Request #1729 · python-kasa/python-kasa · GitHub

Feature/dlklap transport - #1729

Open
tedholtz wants to merge 9 commits into
python-kasa:masterfrom
tedholtz:feature/dlklap-transport
Open

Feature/dlklap transport#1729
tedholtz wants to merge 9 commits into
python-kasa:masterfrom
tedholtz:feature/dlklap-transport

Conversation

tedholtz commented Jul 25, 2026
edited
Loading

Copy link
Copy Markdown

Summary

Adds first-class support for the TP-Link DL100 smart lock via a new
DlklapTransport and a Lock SmartModule. Closes #1693.

The DL100 speaks DLKLAP, a proprietary KLAP variant that runs over plain
HTTP :80 and requires a cloud-assisted handshake0 step to mint a
per-session controlKey before the usual KLAP handshake1/handshake2 and
AES-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:

  1. Cloud login (wap.tplinkcloud.com) → account token + accountId.
  2. handshake0 to the lock over HTTP → a 236-char base64 secret (this
    also wakes the lock's radio).
  3. control-key exchange (*.tplinknbu.com) binds that secret to a
    per-session controlKey.
  4. handshake1 / handshake2 then proceed like KLAP, deriving the AES session
    keys (lsk/ldk/iv) used for /app/request.

Full protocol write-up is in #1693.

Changes

  • kasa/transports/dlklaptransport.py — DlklapTransport(BaseTransport)
    • _DlklapSession. Implements
      handshake0 → control-key → handshake1 → handshake2 → session-key derivation,
      session caching, and single-retry re-handshake on failure. Handshakes are
      serialized (asyncio.Lock) since a second handshake0 invalidates the first
      control key (device error 15033).
  • kasa/smart/modules/lock.py — Lock(SmartModule):
    is_locked, lock(), unlock(), battery, battery_low.
  • Device/discovery wiring — DeviceEncryptionType.Dlklap = "DLKLAP",
    DeviceFamily.SmartTapoLock = "SMART.TAPOLOCK", DeviceType.Lock, the
    SMART.DLKLAP → (SmartProtocol, DlklapTransport) mapping in
    device_factory.py, and the SMART.TAPOLOCK → DeviceType.Lock mapping in
    smartdevice.py.
  • Tests — tests/smart/modules/test_lock.py (mocked, no live device) plus a
    redacted DL100 fixture folded into the parametrized SMART suite.
  • pyproject.toml — adds httpx to dependencies.

Behavior notes for reviewers

  • httpx used directly (not HttpClient) in the transport. This is
    deliberate: the 33-byte binary handshake0 body must be sent verbatim, and
    wrapping middleware re-encodes the text/plain payload and breaks the
    handshake. Happy to revisit if there's a preferred integration point.
  • verify=False is scoped to a single call. The control-key host presents
    TP-Link's private CA (not a public root), so TLS verification is disabled for
    that one request only (# noqa: S501 with a comment). The cloud login
    call, which carries the account password, stays fully verified.
  • Battery lives on the Lock module because the DL100 lacks the
    battery_detect component, so the generic BatterySensor never loads.
  • Lock-status polarity is counterintuitive and verified: 0 = LOCKED
    (bolt extended), 1 = UNLOCKED. Confirmed against decompiled
    EnumDoorLockStatus and the live device — not inverted.

Testing

  • Unit tests: pytest tests/smart/modules/test_lock.py — all pass
    (module presence, is_locked polarity, lock()/unlock() payload with
    owner-forbidden fields excluded, battery).
  • Full suite: pytest tests/ — 22,856 passed, 883 skipped, 0 failures,
    including the test_devtools.py fixture round-trip check.
  • Type/lint: mypy clean on the new files; ruff/ruff format clean.
  • Live device: verified end-to-end against a real DL100 (fw 1.0.17) via
    device_factory.connect() → update() — is_locked=True (lock_status 0),
    battery_level=81, battery_low=False.

Known limitations / follow-ups

  • _ensure_device_id can't disambiguate multiple locks on one account (the
    cloud device list has no LAN IP), so it picks the sole SMART.TAPOLOCK.
    A future config override could address multi-lock setups.
  • The DL100 advertises a speaker component it doesn't implement
    (getVolume → UNKNOWN_METHOD_ERROR); handled gracefully (module marked
    unavailable). Cosmetic device quirk, not a bug.
  • This transport requires cloud round-trips (wap.tplinkcloud.com, tplinknbu.com) mid-handshake. The DL100 mints its controlKey via the cloud (there's no local-only path). The correct local-first implementation necessarily includes these minimal cloud round-trips.

Comment thread kasa/transports/dlklaptransport.py Fixed

codecov Bot commented Jul 25, 2026
edited
Loading

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.89655% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.39%. Comparing base (a29d061) to head (50e83fd).

Files with missing lines Patch % Lines
kasa/transports/dlklaptransport.py 97.17% 4 Missing and 3 partials ⚠️
kasa/smart/modules/lock.py 94.11% 0 Missing and 2 partials ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.



def _sha256(payload: bytes) -> bytes:
return hashlib.sha256(payload).digest() # noqa: S324

tedholtz Jul 27, 2026
edited
Loading

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Copy link
Copy Markdown

Thanks for this work @tedholtz

Hope it gets committed so I can test it out!

rale commented Aug 2, 2026
edited
Loading

Copy link
Copy Markdown

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:

mac = get_mac_address(ip=self._host)
mac = mac.replace(":", "").upper()
locks = [d for d in devices if d.get("deviceMac") == mac]

tedholtz commented Aug 2, 2026
edited
Loading

Copy link
Copy Markdown
Author

I have 2 locks, a DL105 and a DL110. I was able to get them both working ...

@rale thanks for testing.

This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for Tapo DL100 Smart Wi-Fi Door Lock (uses DLKLAP encryption over HTTP)

4 participants


Back | FazBrowse Home | New Git URL