- JavaScript 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| lib | ||
| .gitignore | ||
| index.js | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
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/_deviceIdget filled in after first use).log— object withdebug(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 andcfg._deviceIdisn'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
DlklapApiinstance are serialized internally, so it's safe to callgetDeviceInfo()/setLock()concurrently from your code. - On failure, the client automatically retries once after re-authenticating and re-establishing the session.