No description
  • JavaScript 100%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-23 17:33:20 -06:00
lib Major Update: Multi Device Support 2026-08-23 17:33:20 -06:00
.gitignore Initial 2026-08-23 16:41:17 -06:00
index.js Initial 2026-08-23 16:41:17 -06:00
package-lock.json Guh 2026-08-23 16:45:37 -06:00
package.json Major Update: Multi Device Support 2026-08-23 17:33:20 -06:00
README.md Major Update: Multi Device Support 2026-08-23 17:33:20 -06:00

dlklap.js

Standalone Node.js (CommonJS) client for the TP-Link DLKLAP protocol used by the Tapo DL100 smart lock. Talks to the lock directly over your LAN (no cloud round-trips after the initial device/key discovery).

Install

npm install dlklap.js

Usage

const fs = require('fs');
const { DlklapApi } = require('dlklap.js');

const CACHE_FILE = './dlklap-cache.json';
const cache = fs.existsSync(CACHE_FILE) ? JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')) : {};

const cfg = {
  cloudUsername: 'you@example.com',
  cloudPassword: 'your-tplink-cloud-password',
  // Cached values from a previous run, if any; let the client skip cloud round-trips.
  _terminalUUID: cache._terminalUUID,
  _token: cache._token,
  _accountId: cache._accountId,
};

const log = {
  debug: (m) => console.debug(m),
  warn: (m) => console.warn(m),
  error: (m) => console.error(m),
};

const api = new DlklapApi(cfg, log);

function saveCache(deviceIds) {
  fs.writeFileSync(CACHE_FILE, JSON.stringify({
    _terminalUUID: api.resolvedTerminalUUID,
    _token: api.resolvedToken,
    _accountId: api.resolvedAccountId,
    deviceIds, // your own mapping of lock name -> deviceId
  }, null, 2));
}

async function main() {
  // getDeviceInfo()/setLock() both require a deviceId. Resolve it once from the
  // cloud device list (by alias) and reuse it for every call and every run.
  let deviceIds = cache.deviceIds;
  if (!deviceIds) {
    const devices = await api.listDevices();
    deviceIds = { 'Front Door': devices.find((d) => d.alias === 'Front Door')?.deviceId };
    if (!deviceIds['Front Door']) throw new Error('Lock "Front Door" not found on this TP-Link account.');
  }
  const frontDoorId = deviceIds['Front Door'];

  const info = await api.getDeviceInfo(frontDoorId);
  console.log(info); // { lock_status, battery_percentage, at_low_battery, rssi, ... }

  await api.setLock(true, frontDoorId);  // lock (bolt out)
  await api.setLock(false, frontDoorId); // unlock (bolt in)

  saveCache(deviceIds); // persist any newly-resolved identifiers/token for next run
}

main().catch((err) => console.error(err));

Config (cfg)

Field Required Description
ip no Local IP of the default lock (cfg._deviceId/lockName). Optional — if omitted, it's resolved from the cloud device list the same way as any other deviceId passed to getDeviceInfo/setLock.
cloudUsername yes TP-Link/Tapo account email.
cloudPassword yes TP-Link/Tapo account password.
_terminalUUID no App-instance UUID. Generated automatically on first use; persist and reuse it to avoid regenerating each run.
_deviceId no Cloud device ID for the default lock. Resolved automatically from the account's device list; persist and reuse it to skip that lookup on restart.
_token no Cached cloud auth token. Pass in a previously-resolved token to skip the cloud login call; if it's invalid/expired the client automatically falls back to a fresh login.
_accountId no Cloud account ID paired with _token. Pass in alongside _token.
debugRequests no When true, logs every web request and response (URL, headers, body) to the console. Useful for debugging; leave false/omitted in production since it prints auth headers and tokens.

DlklapApi

new DlklapApi(cfg, log, lockName)

  • cfg — config object described above (mutated in place; _terminalUUID/_deviceId get filled in after first use).
  • log — object with debug(message), warn(message), error(message) methods.
  • lockName — the default device's name/alias in the Tapo app. Only used to pick the right lock when the account has more than one DL100 and cfg._deviceId isn't already cached.

A single instance can control more than one lock on the account: getDeviceInfo()/setLock() accept an optional deviceId to target a device other than the default one, resolving and caching its local IP from the cloud device list on first use (no need to construct a separate DlklapApi per lock).

api.getDeviceInfo(deviceId?)

Returns a Promise resolving to the lock's status object, e.g.:

{ lock_status: 1, battery_percentage: 87, at_low_battery: false, rssi: -52 }

Omit deviceId to query the default lock (cfg._deviceId); pass another account device's ID (from listDevices()) to query a different lock.

api.listDevices()

Returns a Promise resolving to the array of devices on the TP-Link/Tapo account (each with deviceId, alias, deviceModel, etc.), fetched from the cloud device list. Use this to let the caller pick a device instead of relying on lockName matching when an account has more than one.

api.setLock(locked, deviceId?)

Locks (true) or unlocks (false) the deadbolt. Omit deviceId to control the default lock, or pass another account device's ID to control a different one. Returns a Promise<void> that resolves once the lock confirms the state change, or rejects with an Error on failure.

api.resolvedTerminalUUID

Getter returning the generated terminal UUID once resolved (or undefined before first use). Persist this alongside resolvedDeviceId so subsequent runs skip cloud discovery.

api.resolvedDeviceId

Getter returning the default lock's cloud device ID once resolved (or undefined before first use).

api.resolvedToken / api.resolvedAccountId

Getters returning the current cloud auth token and account ID. Read these after calls succeed and persist them alongside _terminalUUID/_deviceId; pass them back in via cfg._token/cfg._accountId next time so the client can skip the cloud login entirely as long as the token stays valid. If the cached token turns out to be invalid/expired, the client detects the failure, discards it, logs in again via the cloud API, and resolvedToken/resolvedAccountId will reflect the new values.

Notes

  • Sessions are cached and reused across calls; a fresh handshake only happens on first use or after a failure.
  • All calls to a given DlklapApi instance are serialized internally, so it's safe to call getDeviceInfo()/setLock() concurrently from your code.
  • On failure, the client automatically retries once after re-authenticating and re-establishing the session.