node-red-contrib-tapo-new-api
Version:
Unofficial node-RED node for connecting to TP-Link Tapo devices with new API. Currently limited to the P100 & P105 & P110 smart plugs and L510E smart bulbs.
959 lines • 95.9 kB
JavaScript
'use strict';
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TapoDevice = exports.TapoDevicesType = exports.SnowflakeId = exports.Chiper = exports.Session = exports.PassthroughProtocol = exports.KlapChiper = exports.KlapSession = exports.KlapProtocol = exports.TapoError = exports.TapoChiper = exports.TapoSession = exports.TapoProtocol = exports.ColorParams = exports.Components = exports.ControlChildParams = exports.MultipleRequestParams = exports.PaginationParams = exports.SecurePassthroughParams = exports.LoginDeviceParamsV2 = exports.LoginDeviceParams = exports.HandshakeParams = exports.TapoResponse = exports.TapoRequest = exports.TapoClient = exports.AuthCredential = exports.TapoProtocolType = exports.ErrorMsg = exports.ErrorCode = exports.supportEnergyUsage = void 0;
// Import modules
const node_util_1 = require("node:util");
const node_crypto_1 = require("node:crypto");
const axios_1 = __importDefault(require("axios"));
const node_http_1 = __importDefault(require("node:http"));
const node_https_1 = __importDefault(require("node:https"));
const uuid_1 = require("uuid");
const local_devices_1 = __importDefault(require("local-devices"));
// Global constants for internal use
const RSA_CIPHER_ALGORITHM = 'rsa';
const AES_CIPHER_ALGORITHM = 'aes-128-cbc';
const PASSPHRASE = "top secret";
const TP_SESSION_COOKIE_NAME = "TP_SESSIONID";
const TP_TEST_USER = "test@tp-link.net";
const TP_TEST_PASSWORD = "test";
const CLOUD_URL = "https://eu-wap.tplinkcloud.com";
const MAX_RETRIES_GUESS = 3;
const AXIOS_TIMEOUT = 1000;
// Constants for supported Energy devices
exports.supportEnergyUsage = ["P110", "P115"];
// Constants for Error
var ErrorCode;
(function (ErrorCode) {
// ----------GENERAL ERROR (1-99)-------------------
ErrorCode[ErrorCode["ERROR_AXIOS_ERROR"] = 1] = "ERROR_AXIOS_ERROR";
ErrorCode[ErrorCode["ERROR_TAPRES_JSON_INVALID"] = 2] = "ERROR_TAPRES_JSON_INVALID";
ErrorCode[ErrorCode["ERROR_AXIOS_FORBID"] = 3] = "ERROR_AXIOS_FORBID";
ErrorCode[ErrorCode["GENERIC_ERROR"] = 99] = "GENERIC_ERROR";
// ----------ENCRIPTION ERROR (101-199)-------------
ErrorCode[ErrorCode["ERROR_KL_ENCRYPT_FMT"] = 101] = "ERROR_KL_ENCRYPT_FMT";
ErrorCode[ErrorCode["ERROR_KL_ENCRYPT_IV_LENGTH"] = 102] = "ERROR_KL_ENCRYPT_IV_LENGTH";
ErrorCode[ErrorCode["ERROR_CH_UNABLE_KEYS"] = 103] = "ERROR_CH_UNABLE_KEYS";
ErrorCode[ErrorCode["ERROR_SNOW_WORKER_ID"] = 104] = "ERROR_SNOW_WORKER_ID";
ErrorCode[ErrorCode["ERROR_SNOW_DATA_CENTER_ID"] = 105] = "ERROR_SNOW_DATA_CENTER_ID";
ErrorCode[ErrorCode["ERROR_SNOW_INVALID_TIME_ID"] = 106] = "ERROR_SNOW_INVALID_TIME_ID";
// ----------SESSION POST ERROR (201-299)-----------
ErrorCode[ErrorCode["ERROR_aSP_bAX_REQ_ERR"] = 201] = "ERROR_aSP_bAX_REQ_ERR";
ErrorCode[ErrorCode["ERROR_aSP_bAX_REQ_FORBID"] = 202] = "ERROR_aSP_bAX_REQ_FORBID";
ErrorCode[ErrorCode["ERROR_aSP_bAX_INVALID_URL"] = 203] = "ERROR_aSP_bAX_INVALID_URL";
// ----------SEND REQUEST ERROR (301-399)-----------
ErrorCode[ErrorCode["ERROR_aSR_bSR_RET_ERR"] = 301] = "ERROR_aSR_bSR_RET_ERR";
ErrorCode[ErrorCode["ERROR_aSR_bSR_MAX_RET"] = 302] = "ERROR_aSR_bSR_MAX_RET";
ErrorCode[ErrorCode["ERROR_aSR_bSP_REQ_ERR"] = 303] = "ERROR_aSR_bSP_REQ_ERR";
ErrorCode[ErrorCode["ERROR_aSR_bSP_REJ"] = 304] = "ERROR_aSR_bSP_REJ";
ErrorCode[ErrorCode["ERROR_aSR_DEV_FORBID"] = 305] = "ERROR_aSR_DEV_FORBID";
ErrorCode[ErrorCode["ERROR_aSR_DEV_GENERAL"] = 399] = "ERROR_aSR_DEV_GENERAL";
// ----------HANDSHAKE ERROR (401-499)--------------
ErrorCode[ErrorCode["ERROR_aPH2_bSP_HSK_ERROR"] = 401] = "ERROR_aPH2_bSP_HSK_ERROR";
ErrorCode[ErrorCode["ERROR_aPH1_bSP_HSK_ERROR"] = 402] = "ERROR_aPH1_bSP_HSK_ERROR";
ErrorCode[ErrorCode["ERROR_aPH2_bSP_HSK_REJ"] = 411] = "ERROR_aPH2_bSP_HSK_REJ";
ErrorCode[ErrorCode["ERROR_aPH_bSP_HSK_REJ"] = 412] = "ERROR_aPH_bSP_HSK_REJ";
ErrorCode[ErrorCode["ERROR_aPH1_bSP_HSK_MISSMATCH"] = 421] = "ERROR_aPH1_bSP_HSK_MISSMATCH";
ErrorCode[ErrorCode["ERROR_aPH1_bSP_HSK_FORBID"] = 431] = "ERROR_aPH1_bSP_HSK_FORBID";
ErrorCode[ErrorCode["ERROR_aPH_bSP_HSK_FORBID"] = 432] = "ERROR_aPH_bSP_HSK_FORBID";
ErrorCode[ErrorCode["ERROR_aPH_bPH1_HSK_ERROR"] = 441] = "ERROR_aPH_bPH1_HSK_ERROR";
ErrorCode[ErrorCode["ERROR_aSR_bPH_HSK_ERROR"] = 442] = "ERROR_aSR_bPH_HSK_ERROR";
ErrorCode[ErrorCode["ERROR_aLG_bS_TOKEN_NOT_FOUND"] = 451] = "ERROR_aLG_bS_TOKEN_NOT_FOUND";
ErrorCode[ErrorCode["ERROR_aLG_bPH_HSK_TIMEOUT"] = 452] = "ERROR_aLG_bPH_HSK_TIMEOUT";
ErrorCode[ErrorCode["ERROR_aLG_bS_TOKEN_ERROR"] = 453] = "ERROR_aLG_bS_TOKEN_ERROR";
ErrorCode[ErrorCode["ERROR_aLG_bPH_HSK_ERROR"] = 454] = "ERROR_aLG_bPH_HSK_ERROR";
// ----------GUESS PROTOCOL (501-599)--------------
ErrorCode[ErrorCode["ERROR_aGP_INCOMPLETE"] = 501] = "ERROR_aGP_INCOMPLETE";
ErrorCode[ErrorCode["ERROR_aGP_GUESS"] = 502] = "ERROR_aGP_GUESS";
// ----------FUNCTIONAL ERROR----------------------
ErrorCode[ErrorCode["ERROR_FUNC_GENERAL"] = 601] = "ERROR_FUNC_GENERAL";
ErrorCode[ErrorCode["ERROR_CLOUD_CONN_REJ"] = 602] = "ERROR_CLOUD_CONN_REJ";
ErrorCode[ErrorCode["ERROR_DEVICE_INFO"] = 603] = "ERROR_DEVICE_INFO";
ErrorCode[ErrorCode["ERROR_CLOUD_NO_DEVICE_LIST"] = 604] = "ERROR_CLOUD_NO_DEVICE_LIST";
ErrorCode[ErrorCode["ERROR_ALIAS_NOT_FOUND"] = 605] = "ERROR_ALIAS_NOT_FOUND";
ErrorCode[ErrorCode["ERROR_FUNC_VALID_COLOR"] = 651] = "ERROR_FUNC_VALID_COLOR";
ErrorCode[ErrorCode["ERROR_FUNC_TEMP_COLOR"] = 652] = "ERROR_FUNC_TEMP_COLOR";
ErrorCode[ErrorCode["ERROR_FUNC_HEX_COLOR"] = 653] = "ERROR_FUNC_HEX_COLOR";
ErrorCode[ErrorCode["ERROR_FUNC_KEY_LENGTH"] = -1010] = "ERROR_FUNC_KEY_LENGTH";
ErrorCode[ErrorCode["ERROR_FUNC_BAD_CREDENTIALS"] = -1501] = "ERROR_FUNC_BAD_CREDENTIALS";
ErrorCode[ErrorCode["ERROR_FUNC_BAD_REQUEST"] = -1002] = "ERROR_FUNC_BAD_REQUEST";
ErrorCode[ErrorCode["ERROR_FUNC_BAD_JSON"] = -1003] = "ERROR_FUNC_BAD_JSON";
ErrorCode[ErrorCode["ERROR_FUNC_WRONG_EMAIL"] = -20601] = "ERROR_FUNC_WRONG_EMAIL";
ErrorCode[ErrorCode["ERROR_FUNC_CLOUD_TOKEN_EXPIRED"] = -20675] = "ERROR_FUNC_CLOUD_TOKEN_EXPIRED";
ErrorCode[ErrorCode["ERROR_FUNC_DEV_TOKEN_EXPIRED"] = 9999] = "ERROR_FUNC_DEV_TOKEN_EXPIRED";
ErrorCode[ErrorCode["ERROR_FUNC_UNEXPECTED"] = 19999] = "ERROR_FUNC_UNEXPECTED";
})(ErrorCode = exports.ErrorCode || (exports.ErrorCode = {}));
var ErrorMsg;
(function (ErrorMsg) {
// ----------GENERAL ERROR (1-99)-------------------
ErrorMsg["ERROR_AXIOS_ERROR"] = "Axios error: ";
ErrorMsg["ERROR_TAPRES_JSON_INVALID"] = "Invalid JSON answer: ";
ErrorMsg["ERROR_AXIOS_FORBID"] = "Negotiation error: ";
ErrorMsg["GENERIC_ERROR"] = "General error: ";
// ----------ENCRIPTION ERROR (101-199)-------------
ErrorMsg["ERROR_KL_ENCRYPT_FMT"] = "Encryption error: ";
ErrorMsg["ERROR_KL_ENCRYPT_IV_LENGTH"] = "Encryption error: ";
ErrorMsg["ERROR_CH_UNABLE_KEYS"] = "Encryption error: ";
ErrorMsg["ERROR_SNOW_WORKER_ID"] = "Encryption error: ";
ErrorMsg["ERROR_SNOW_DATA_CENTER_ID"] = "Encryption error: ";
ErrorMsg["ERROR_SNOW_INVALID_TIME_ID"] = "Encryption error: ";
// ----------SESSION POST ERROR (201-299)-----------
ErrorMsg["ERROR_aSP_bAX_REQ_ERR"] = "Device comm error: ";
ErrorMsg["ERROR_aSP_bAX_REQ_FORBID"] = "Device comm rejected: ";
ErrorMsg["ERROR_aSP_bAX_INVALID_URL"] = "URL not valid: ";
// ----------SEND REQUEST ERROR (301-399)-----------
ErrorMsg["ERROR_aSR_bSR_RET_ERR"] = "Instant retry error: ";
ErrorMsg["ERROR_aSR_bSR_MAX_RET"] = "Max retries reached: ";
ErrorMsg["ERROR_aSR_bSP_REQ_ERR"] = "Device request error: ";
ErrorMsg["ERROR_aSR_bSP_REJ"] = "Device comm rejected: ";
ErrorMsg["ERROR_aSR_DEV_FORBID"] = "Device comm rejected: ";
ErrorMsg["ERROR_aSR_DEV_GENERAL"] = "Device comm error: ";
// ----------HANDSHAKE ERROR (401-499)--------------
ErrorMsg["ERROR_aPH2_bSP_HSK_ERROR"] = "Handshake error: ";
ErrorMsg["ERROR_aPH1_bSP_HSK_ERROR"] = "Handshake error: ";
ErrorMsg["ERROR_aPH2_bSP_HSK_REJ"] = "Handshare rejected: ";
ErrorMsg["ERROR_aPH_bSP_HSK_REJ"] = "Handshare rejected: ";
ErrorMsg["ERROR_aPH1_bSP_HSK_MISSMATCH"] = "Handshake error: ";
ErrorMsg["ERROR_aPH1_bSP_HSK_FORBID"] = "Handshare rejected: ";
ErrorMsg["ERROR_aPH_bSP_HSK_FORBID"] = "Handshare rejected: ";
ErrorMsg["ERROR_aPH_bPH1_HSK_ERROR"] = "Handshake error: ";
ErrorMsg["ERROR_aSR_bPH_HSK_ERROR"] = "Handshake error: ";
ErrorMsg["ERROR_aLG_bS_TOKEN_NOT_FOUND"] = "Handshake error: ";
ErrorMsg["ERROR_aLG_bPH_HSK_TIMEOUT"] = "Handshake timeout: ";
ErrorMsg["ERROR_aLG_bS_TOKEN_ERROR"] = "Handshake error: ";
ErrorMsg["ERROR_aLG_bPH_HSK_ERROR"] = "Handshake error: ";
// ----------GUESS PROTOCOL (501-599)--------------
ErrorMsg["ERROR_aGP_INCOMPLETE"] = "Protocol not detected: ";
ErrorMsg["ERROR_aGP_GUESS"] = "Protocol not detected: ";
// ----------FUNCTIONAL ERROR----------------------
ErrorMsg["ERROR_FUNC_GENERAL"] = "General functional error: ";
ErrorMsg["ERROR_CLOUD_CONN_REJ"] = "Cloud connection rejected: ";
ErrorMsg["ERROR_DEVICE_INFO"] = "Device comm error: ";
ErrorMsg["ERROR_CLOUD_NO_DEVICE_LIST"] = "Cloud empty list: ";
ErrorMsg["ERROR_ALIAS_NOT_FOUND"] = "Alias not found: ";
ErrorMsg["ERROR_FUNC_VALID_COLOR"] = "Color not valid: ";
ErrorMsg["ERROR_FUNC_TEMP_COLOR"] = "Color not valid: ";
ErrorMsg["ERROR_FUNC_HEX_COLOR"] = "Color not valid: ";
ErrorMsg["ERROR_FUNC_KEY_LENGTH"] = "Encryption error: ";
ErrorMsg["ERROR_FUNC_BAD_CREDENTIALS"] = "Bad credentials: ";
ErrorMsg["ERROR_FUNC_BAD_REQUEST"] = "Device request error: ";
ErrorMsg["ERROR_FUNC_BAD_JSON"] = "Invalid JSON answer: ";
ErrorMsg["ERROR_FUNC_WRONG_EMAIL"] = "Bad credentials: ";
ErrorMsg["ERROR_FUNC_CLOUD_TOKEN_EXPIRED"] = "Token expired: ";
ErrorMsg["ERROR_FUNC_DEV_TOKEN_EXPIRED"] = "Token expired: ";
ErrorMsg["ERROR_FUNC_UNEXPECTED"] = "Unexpected error: ";
})(ErrorMsg = exports.ErrorMsg || (exports.ErrorMsg = {}));
// ****************************************************
// *** TAPO CLASSES TO DEFINE TAPO CLIENT SESSIONS ***
// ****************************************************
// Class for TapoProtocol
var TapoProtocolType;
(function (TapoProtocolType) {
TapoProtocolType[TapoProtocolType["PASSTHROUGH"] = 1] = "PASSTHROUGH";
TapoProtocolType[TapoProtocolType["KLAP"] = 2] = "KLAP";
TapoProtocolType[TapoProtocolType["AUTO"] = 3] = "AUTO";
})(TapoProtocolType = exports.TapoProtocolType || (exports.TapoProtocolType = {}));
// Class for Authentication credentials
class AuthCredential {
constructor(user, passwd) {
this.username = (typeof (user) == 'undefined' ? '' : user);
this.password = (typeof (passwd) == 'undefined' ? '' : passwd);
// Set the properties to false to avoid any change or access once created
Object.defineProperty(this, 'username', { enumerable: false, writable: false, configurable: false });
Object.defineProperty(this, 'password', { enumerable: false, writable: false, configurable: false });
}
}
exports.AuthCredential = AuthCredential;
// Class for TapoClient - connection to every Tapo device
class TapoClient {
// Constructor to initialize the class
constructor(auth_credential, url, protocol, terminal_random, keep_alive, debug) {
this._auth_credential = auth_credential;
let myURL = new URL("http:\\example.com");
try {
const myURL2 = new URL(url);
myURL = myURL2;
}
catch (e) {
myURL.protocol = 'http';
myURL.hostname = url;
myURL.port = '80';
myURL.pathname = "/app";
}
this._url = myURL.href;
this._protocol_type = (typeof (protocol) === undefined ? TapoProtocolType.AUTO : protocol);
this._protocol = null;
this._terminal_random = ((typeof (terminal_random) == 'undefined') ? false : terminal_random);
this._debug = (typeof (debug) == 'undefined' ? false : debug);
this._keep_alive = (typeof (keep_alive) == 'undefined' ? true : keep_alive);
// Create the list of functions
this.actions = { 'list_methods': () => { return this.list_methods(); },
'perform_handshake': (protocol) => { return this.perform_handshake(protocol); },
'get_component_negotiation': (protocol) => { return this.get_component_negotiation(protocol); },
'get_device_info': (protocol) => { return this.get_device_info(protocol); },
'get_current_power': (protocol) => { return this.get_current_power(protocol); },
'get_energy_usage': (protocol) => { return this.get_energy_usage(protocol); },
'get_child_device_list': (params, protocol) => { return this.get_child_device_list(params, protocol); },
'execute_raw_request': (request, protocol, retry) => { return this.execute_raw_request(request, protocol, retry); },
'send_request': (request, protocol) => { return this.send_request(request, protocol); },
'set_device_info': (params, protocol) => { return this.set_device_info(params, protocol); },
'turn_onoff_device': (params, protocol) => { return this.turn_onoff_device(params, protocol); },
'set_color_device': (params, protocol) => { return this.set_color_device(params, protocol); },
'set_brightness_device': (params, protocol) => { return this.set_brightness_device(params, protocol); } };
// Set the properties to false to avoid any change or access once created
if (!this._debug) {
Object.defineProperty(this, '_auth_credential', { enumerable: false });
Object.defineProperty(this, '_protocol', { enumerable: false });
Object.defineProperty(this, '_protocol_type', { enumerable: false });
Object.defineProperty(this, '_terminal_random', { enumerable: false });
Object.defineProperty(this, '_debug', { enumerable: false });
Object.defineProperty(this, '_keep_alive', { enumerable: false });
}
}
// Private methods to setup the protocol
async _initialize_protocol_if_needed(protocol) {
const proto = (typeof (protocol) == 'undefined' ? this._protocol_type : protocol);
if ((this._protocol == null) || ((this._protocol._protocol_type != proto) && (proto != TapoProtocolType.AUTO)))
this._protocol = await this._guess_protocol(proto);
}
async _guess_protocol(protocol) {
// Process parameters
const proto = (typeof (protocol) == 'undefined' ? this._protocol_type : protocol);
// Process the ptotocol if it is not AUTO
if (proto == TapoProtocolType.PASSTHROUGH) {
// Try PASSTHROUGH protocol
try {
// Set PASSTHROGH and get the components
if (this._debug)
console.debug("Set protocol to PASSTHROUGH");
this._protocol = new PassthroughProtocol(this._auth_credential, this._url, this._terminal_random, this._keep_alive, this._debug);
const resp = await this.get_component_negotiation(proto, MAX_RETRIES_GUESS);
}
catch (error) {
// Close this protocol and try again without keep_alive
this.close();
this._keep_alive = false;
try {
// Set PASSTHROGH and get the components
if (this._debug)
console.debug("Set protocol to PASSTHROUGH - Keep alive forced to false");
this._protocol = new PassthroughProtocol(this._auth_credential, this._url, this._terminal_random, this._keep_alive, this._debug);
const resp = await this.get_component_negotiation(proto, MAX_RETRIES_GUESS);
}
catch (error) {
this.close();
throw new TapoError(ErrorMsg[ErrorCode[ErrorCode.ERROR_aGP_INCOMPLETE]] + 'Negotiation not completed', error, ErrorCode.ERROR_aGP_INCOMPLETE, this._guess_protocol.name);
}
}
// Return the final protocol
return this._protocol;
}
else if (proto == TapoProtocolType.KLAP) {
// Try KLAP protocol
try {
// Set KLAP and get the components
if (this._debug)
console.debug("Set protocol to KLAP");
this._protocol = new KlapProtocol(this._auth_credential, this._url, this._terminal_random, this._keep_alive, this._debug);
const resp = await this.get_component_negotiation(proto, MAX_RETRIES_GUESS);
}
catch (error) {
// Close this protocol and try again without keep_alive
this.close();
this._keep_alive = false;
try {
// Set KLAP and get the components
if (this._debug)
console.debug("Set protocol to KLAP - Keep alive forced to false");
this._protocol = new KlapProtocol(this._auth_credential, this._url, this._terminal_random, this._keep_alive, this._debug);
const resp = await this.get_component_negotiation(proto, MAX_RETRIES_GUESS);
}
catch (error) {
this.close();
throw new TapoError(ErrorMsg[ErrorCode[ErrorCode.ERROR_aGP_INCOMPLETE]] + 'Negotiation not completed', error, ErrorCode.ERROR_aGP_INCOMPLETE, this._guess_protocol.name);
}
}
// Return the final protocol
return this._protocol;
}
else {
// Try first with PassthroughProtocol
const ka_bck = this._keep_alive;
try {
// Set Passthrough and get the components
if (this._debug)
console.debug("Trying first with PassthroughProtocol");
this._protocol = new PassthroughProtocol(this._auth_credential, this._url, this._terminal_random, this._keep_alive, this._debug);
const resp = await this.get_component_negotiation(TapoProtocolType.PASSTHROUGH, MAX_RETRIES_GUESS);
}
catch (error) {
// Close this protocol and try again without keep_alive
this.close();
this._keep_alive = false;
try {
// Set Passthrough without keep_alive and get the components
if (this._debug)
console.debug("Trying again with PassthroughProtocol - Keep alive forced to false");
this._protocol = new PassthroughProtocol(this._auth_credential, this._url, this._terminal_random, this._keep_alive, this._debug);
const resp = await this.get_component_negotiation(proto, MAX_RETRIES_GUESS);
}
catch (error) {
// Try to fallback to KLAP
this.close();
this._keep_alive = ka_bck;
try {
// Set KLAP and get the components
if (this._debug)
console.debug("Default protocol not working. Fallback to KLAP");
this._protocol = new KlapProtocol(this._auth_credential, this._url, this._terminal_random, this._keep_alive, this._debug);
const resp_klap = await this.get_component_negotiation(TapoProtocolType.KLAP, MAX_RETRIES_GUESS);
}
catch (error) {
// Set KLAP and try again without keep_alive
this.close();
this._keep_alive = false;
try {
// Set KLAP and get the components
if (this._debug)
console.debug("Default protocol not working. Fallback to KLAP - Keep alive forced to false");
this._protocol = new KlapProtocol(this._auth_credential, this._url, this._terminal_random, this._keep_alive, this._debug);
const resp = await this.get_component_negotiation(proto, MAX_RETRIES_GUESS);
}
catch (error) {
this.close();
throw new TapoError(ErrorMsg[ErrorCode[ErrorCode.ERROR_aGP_INCOMPLETE]] + 'Negotiation not completed', error, ErrorCode.ERROR_aGP_INCOMPLETE, this._guess_protocol.name);
}
}
}
}
;
// Return the final protocol
return this._protocol;
}
}
// Public protocol exposed methods for external control
async perform_handshake(protocol) {
const proto = (typeof (protocol) == 'undefined' ? this._protocol_type : protocol);
await this._initialize_protocol_if_needed(proto);
this._protocol._session = await this._protocol.perform_handshake();
return this;
}
// Public method to expose protocol send request
async send_request(request, protocol) {
const proto = (typeof (protocol) == 'undefined' ? this._protocol_type : protocol);
await this._initialize_protocol_if_needed(proto);
if ((this._protocol._session == null) || (!this._protocol._session.handshake_complete) || (this._protocol._session.is_handshake_session_expired())) {
this._protocol._session = await this._protocol.perform_handshake();
}
return this._protocol.send_request(request);
}
// Public method to close the session
close() {
if (this._protocol != null)
this._protocol.close(false);
}
// Public methods to execute raw request
async execute_raw_request(request, protocol, retry) {
const proto = (typeof (protocol) == 'undefined' ? this._protocol_type : protocol);
await this._initialize_protocol_if_needed(proto);
return await this._protocol.send_request(request, ((typeof (retry) == 'undefined') ? undefined : retry))
.then((value) => {
return value.result;
});
}
// Public methods to execute different kind of requests
async get_component_negotiation(protocol, retry) {
const proto = (typeof (protocol) == 'undefined' ? this._protocol_type : protocol);
return new Components().try_from_json(await this.execute_raw_request(new TapoRequest().component_negotiation(), proto, retry));
}
async get_device_info(protocol) {
const proto = (typeof (protocol) == 'undefined' ? this._protocol_type : protocol);
return await this.execute_raw_request(new TapoRequest().get_device_info(), proto);
}
async get_current_power(protocol) {
const proto = (typeof (protocol) == 'undefined' ? this._protocol_type : protocol);
return await this.execute_raw_request(new TapoRequest().get_current_power(), proto);
}
async get_energy_usage(protocol) {
const proto = (typeof (protocol) == 'undefined' ? this._protocol_type : protocol);
return await this.execute_raw_request(new TapoRequest().get_energy_usage(), proto);
}
async set_device_info(params, protocol) {
const proto = (typeof (protocol) == 'undefined' ? this._protocol_type : protocol);
return await this.execute_raw_request(new TapoRequest().set_device_info(params), proto);
}
async turn_onoff_device(params, protocol) {
const proto = (typeof (protocol) == 'undefined' ? this._protocol_type : protocol);
return await this.execute_raw_request(new TapoRequest().turn_onoff_device((typeof (params) == 'undefined') ? false : params), proto);
}
async set_color_device(params, protocol) {
const proto = (typeof (protocol) == 'undefined' ? this._protocol_type : protocol);
return await this.execute_raw_request(new TapoRequest().set_color_device((typeof (params) == 'undefined') ? "white" : params), proto);
}
async set_brightness_device(params, protocol) {
const proto = (typeof (protocol) == 'undefined' ? this._protocol_type : protocol);
return await this.execute_raw_request(new TapoRequest().set_brightness_device((typeof (params) == 'undefined') ? 100 : params), proto);
}
async get_child_device_list(params, protocol) {
const proto = (typeof (protocol) == 'undefined' ? this._protocol_type : protocol);
return await this.execute_raw_request(new TapoRequest().get_child_device_list((typeof (params) == 'undefined') ? 0 : params), proto);
}
list_methods() {
return Object.keys(this.actions);
}
}
exports.TapoClient = TapoClient;
// Class for different TapoRequests
class TapoRequest {
// Constructor to initialize the class
constructor(method, params) {
this.method = (typeof (method) == 'undefined' ? undefined : method);
this.params = (typeof (params) == 'undefined' ? undefined : params);
}
// Define the different methods
handshake(params) {
this.method = "handshake";
this.params = params;
return this;
}
login(credential, v2) {
this.method = "login_device";
this.params = (((typeof (v2) == 'undefined') || (!v2)) ? new LoginDeviceParams(credential.username, credential.password) : new LoginDeviceParamsV2(credential.username, credential.password));
return this;
}
cloud_login(credential) {
this.method = "login";
this.params = { "appType": "Tapo_Android", "cloudPassword": credential.password, "cloudUserName": credential.username, "terminalUUID": (0, uuid_1.v4)() };
return this;
}
cloud_list_devices() {
this.method = "getDeviceList";
this.params = undefined;
return this;
}
secure_passthrough(params) {
this.method = "securePassthrough";
this.params = params;
return this;
}
get_device_info() {
this.method = "get_device_info";
this.params = undefined;
return this;
}
get_device_usage() {
this.method = "get_device_usage";
this.params = undefined;
return this;
}
get_energy_usage() {
this.method = "get_energy_usage";
this.params = undefined;
return this;
}
set_device_info(params) {
this.method = "set_device_info";
this.params = params;
return this;
}
turn_onoff_device(params) {
this.method = "set_device_info";
this.params = { "device_on": params };
return this;
}
set_color_device(params) {
this.method = "set_device_info";
this.params = new ColorParams().get_color(params);
return this;
}
set_brightness_device(params) {
this.method = "set_device_info";
this.params = { "brightness": Math.max(0, Math.min(params, 100)) };
return this;
}
get_current_power() {
this.method = "get_current_power";
this.params = undefined;
return this;
}
// public set_lighting_effect(effect: LightEffect) : TapoRequest {
// return new TapoRequest("set_lighting_effect", effect);
// }
get_child_device_list(start_index) {
this.method = "get_child_device_list";
this.params = new PaginationParams(start_index);
return this;
}
get_child_device_component_list() {
this.method = "get_child_device_component_list";
this.params = undefined;
return this;
}
multiple_request(requests) {
this.method = "multipleRequest";
this.params = undefined;
return this;
}
control_child(device_id, request) {
this.method = "control_child";
this.params = new ControlChildParams(device_id, request);
return this;
}
// public get_child_event_logs(trigger_log_params: GetTriggerLogsParams) : TapoRequest {
// return new TapoRequest("get_trigger_logs", trigger_log_params);
// }
get_temperature_humidity_records() {
this.method = "get_temp_humidity_records";
this.params = undefined;
return this;
}
component_negotiation() {
this.method = "component_nego";
this.params = undefined;
return this;
}
with_request_id(request_id) {
this.requestID = request_id;
return this;
}
with_request_time_millis(t) {
this.request_time_millis = t;
return this;
}
with_terminal_uuid(uuid) {
this.terminal_uuid = uuid;
return this;
}
get_params() {
return this.params;
}
get_method() {
return this.method;
}
__eq__(other) {
if (!(other instanceof TapoRequest)) {
return false;
}
else {
return ((this.method === other.method) && (this.params === other.params));
}
}
}
exports.TapoRequest = TapoRequest;
// Class for different TapoResponses
class TapoResponse {
// Constructor of the class
constructor(err, res, mg) {
this.error_code = (typeof (err) == 'undefined' ? 0 : err);
this.result = (typeof (res) == 'undefined' ? null : res);
this.msg = (typeof (mg) == 'undefined' ? '' : mg);
}
// Methods defined in the class
async try_from_json(json) {
// Process the response from server to get the three fields
this.error_code = (typeof (json["error_code"]) == 'undefined' ? 0 : json["error_code"]);
this.result = (typeof (json["result"]) == 'undefined' ? null : json["result"]);
this.msg = (typeof (json["msg"]) == 'undefined' ? 'No message' : json["msg"]);
// Return this same class instance
await this.check_Error();
return this;
}
// Private method to check functional errors
async check_Error() {
// Throw an error in case it is different from 0
switch (this.error_code) {
case 0: break;
case -1010: throw new TapoError(ErrorMsg[ErrorCode[ErrorCode.ERROR_FUNC_KEY_LENGTH]] + "Invalid public key length", null, ErrorCode.ERROR_FUNC_KEY_LENGTH, this.check_Error.name);
case -1501: throw new TapoError(ErrorMsg[ErrorCode[ErrorCode.ERROR_FUNC_BAD_CREDENTIALS]] + "Invalid request or credentials", null, ErrorCode.ERROR_FUNC_BAD_CREDENTIALS, this.check_Error.name);
case -1001: throw new TapoError(ErrorMsg[ErrorCode[ErrorCode.ERROR_FUNC_BAD_REQUEST]] + "Incorrect request", null, ErrorCode.ERROR_FUNC_BAD_REQUEST, this.check_Error.name);
case -1002: throw new TapoError(ErrorMsg[ErrorCode[ErrorCode.ERROR_FUNC_BAD_REQUEST]] + "Incorrect request", null, ErrorCode.ERROR_FUNC_BAD_REQUEST, this.check_Error.name);
case -1003: throw new TapoError(ErrorMsg[ErrorCode[ErrorCode.ERROR_FUNC_BAD_JSON]] + "JSON format error", null, ErrorCode.ERROR_FUNC_BAD_JSON, this.check_Error.name);
case -20601: throw new TapoError(ErrorMsg[ErrorCode[ErrorCode.ERROR_FUNC_WRONG_EMAIL]] + "Incorrect email or password", null, ErrorCode.ERROR_FUNC_WRONG_EMAIL, this.check_Error.name);
case -20675: throw new TapoError(ErrorMsg[ErrorCode[ErrorCode.ERROR_FUNC_CLOUD_TOKEN_EXPIRED]] + "Cloud token expired or invalid", null, ErrorCode.ERROR_FUNC_CLOUD_TOKEN_EXPIRED, this.check_Error.name);
case 9999: throw new TapoError(ErrorMsg[ErrorCode[ErrorCode.ERROR_FUNC_DEV_TOKEN_EXPIRED]] + "Device token expired or invalid", null, ErrorCode.ERROR_FUNC_DEV_TOKEN_EXPIRED, this.check_Error.name);
default: throw new TapoError(ErrorMsg[ErrorCode[ErrorCode.ERROR_FUNC_UNEXPECTED]] + `Unexpected Error Code: ${this.error_code} (${this.msg})`, null, ErrorCode.ERROR_FUNC_UNEXPECTED, this.check_Error.name);
}
}
}
exports.TapoResponse = TapoResponse;
// Class for different Request parameters
class HandshakeParams {
//Constructor to initialize the class
constructor(key) {
this.key = key;
}
}
exports.HandshakeParams = HandshakeParams;
class LoginDeviceParams {
// Constructor to initialize the class
constructor(user, pass) {
this.username = Buffer.from((0, node_crypto_1.createHash)("sha1").update(user).digest('hex')).toString('base64');
this.password = Buffer.from(pass).toString('base64');
}
}
exports.LoginDeviceParams = LoginDeviceParams;
class LoginDeviceParamsV2 {
// Constructor to initialize the class
constructor(user, pass) {
this.username = Buffer.from((0, node_crypto_1.createHash)("sha1").update(user).digest('hex')).toString('base64');
this.password2 = Buffer.from((0, node_crypto_1.createHash)("sha1").update(pass).digest('hex')).toString('base64');
}
}
exports.LoginDeviceParamsV2 = LoginDeviceParamsV2;
class SecurePassthroughParams {
// Constructor
constructor(request) {
this.request = request;
}
}
exports.SecurePassthroughParams = SecurePassthroughParams;
class PaginationParams {
// Constructor to initialize the class
constructor(idx) {
this.start_index = idx;
}
}
exports.PaginationParams = PaginationParams;
class MultipleRequestParams {
}
exports.MultipleRequestParams = MultipleRequestParams;
class ControlChildParams {
// Constructor to initialize the class
constructor(device, request) {
this.device_id = device;
this.requestData = request;
}
}
exports.ControlChildParams = ControlChildParams;
class Components {
// Constructor of the class
constructor(list) {
this.component_list = list;
}
// Methods of the class
try_from_json(data) {
const components = data["component_list"] || [];
this.component_list = components.reduce((acc, c) => {
acc[c["id"]] = c["ver_code"];
return acc;
}, {});
return this;
}
}
exports.Components = Components;
class ColorParams {
constructor() {
// Define constants for preset colors
this.preset = {
blue: {
hue: 240,
saturation: 100,
color_temp: 0
},
red: {
hue: 0,
saturation: 100,
color_temp: 0
},
yellow: {
hue: 60,
saturation: 100,
color_temp: 0
},
green: {
hue: 120,
saturation: 100,
color_temp: 0
},
white: {
color_temp: 4500
},
daylightwhite: {
color_temp: 5500
},
warmwhite: {
color_temp: 2700
}
};
}
HEXtoHSL(hex) {
// Check valid hex color
if (hex.toLowerCase() === '#000000')
throw new TapoError(ErrorMsg[ErrorCode[ErrorCode.ERROR_FUNC_HEX_COLOR]] + 'Cannot set light to black', null, ErrorCode.ERROR_FUNC_HEX_COLOR, this.HEXtoHSL.name);
let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
// Return i fnull color
if (result == null)
return;
// Get RGB components
let r = parseInt(result[1], 16);
let g = parseInt(result[2], 16);
let b = parseInt(result[3], 16);
r /= 255, g /= 255, b /= 255;
// Get Hue, Saturation and Brightness components
let max = Math.max(r, g, b), min = Math.min(r, g, b);
let h = 0, s = 0, l = (max + min) / 2;
if (max == min) {
h = s = 0; // achromatic
}
else {
let d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
s = s * 100;
s = Math.round(s);
l = l * 100;
l = Math.round(l);
h = Math.round(360 * h);
// Return hue, saturation and brightness
return { hue: h, saturation: s, brightness: l };
}
temperature(temp) {
let t = parseInt(temp.slice(0, -1));
if (t < 2500 || t > 6500)
throw new TapoError(ErrorMsg[ErrorCode[ErrorCode.ERROR_FUNC_TEMP_COLOR]] + 'Colour temperature should be between 2500K and 6500K.', null, ErrorCode.ERROR_FUNC_TEMP_COLOR, this.temperature.name);
return { color_temp: t };
}
get_color(color) {
color = color.toLowerCase();
if (color.startsWith('#'))
return this.HEXtoHSL(color);
if (color.endsWith('k'))
return this.temperature(color);
if (Object.keys(this.preset).includes(color))
return this.preset[color] || "";
throw new TapoError(ErrorMsg[ErrorCode[ErrorCode.ERROR_FUNC_VALID_COLOR]] + 'Invalid Color', null, ErrorCode.ERROR_FUNC_VALID_COLOR, this.get_color.name);
}
}
exports.ColorParams = ColorParams;
// *********************************************
// *** TAPO CLASSES TO DEFINE TAPO PROTOCOL ***
// *********************************************
// Class for abstract TapoProtocol
class TapoProtocol {
}
exports.TapoProtocol = TapoProtocol;
// Class for abstract TapoSession
class TapoSession {
}
exports.TapoSession = TapoSession;
// Class for abstract TapoChiper
class TapoChiper {
}
exports.TapoChiper = TapoChiper;
// Class to manage TapoErrors
class TapoError {
// Constructor of the class
constructor(message, track, code, agent) {
// Define first the non-recursive properties
this.error_code = (typeof (code) == 'undefined') ? ErrorCode.GENERIC_ERROR : code;
this.message = (typeof (message) == 'undefined' ? '' : message);
this.agent = agent;
// Define the recursive ones taking care of the non-recursive already defined and availability ot track and track.cause
this.track = ((typeof (track) == 'undefined') || (track == null)) ? undefined : track;
if ((typeof (track) == 'undefined') || (track == null) || (!(track instanceof TapoError))) {
this.cause = { error_code: this.error_code, message: this.message, agent: this.agent };
}
else if ((typeof (track.cause) == 'undefined') || (track.cause == null)) {
this.cause = track.get_current_cause();
}
else {
this.cause = track.cause;
}
// Delete the empty objects on track and cause
if ((typeof (this.track) != 'undefined') && (track != null)) {
delete this.track.cause;
}
else {
delete this.track;
}
}
//Public method to transfer Axios to Tapo
axios_to_tapo(axios) {
if (axios.code == "ERR_BAD_REQUEST") {
this.error_code = ErrorCode.ERROR_AXIOS_FORBID;
}
else {
this.error_code = ErrorCode.ERROR_AXIOS_ERROR;
}
this.message = ErrorMsg[ErrorCode[this.error_code]] + axios.code + ' - ' + axios.message;
this.agent = axios.name;
this.cause = { message: this.message, error_code: this.error_code, agent: this.agent };
return this;
}
// Public method to get the Cause
get_current_cause() {
return { error_code: this.error_code, message: this.message, agent: this.agent };
}
}
exports.TapoError = TapoError;
// *********************************************
// *** KLAP CLASSES TO DEFINE KLAP PROTOCOL ***
// *********************************************
// Class for Klap Protocol definition
class KlapProtocol extends TapoProtocol {
// Constructor to initialize the class
constructor(auth_credential, url, terminal_random, keep_alive, debug) {
super();
this._base_url = url;
this._host = (new URL(this._base_url)).hostname;
this._auth_credential = auth_credential;
this.local_auth_hash = this.generate_auth_hash(this._auth_credential);
this._local_seed = null;
this._jar = null;
this._session = null;
this._protocol_type = TapoProtocolType.KLAP;
this._terminal_random = ((typeof (terminal_random) == 'undefined') ? false : terminal_random);
this._request_id_generator = new SnowflakeId(1, 1);
this._keep_alive = ((typeof (keep_alive) == 'undefined') ? true : keep_alive);
this._debug = (typeof (debug) == 'undefined' ? false : debug);
try {
this._http_agent = (new URL(url).protocol == 'https:' ? new node_https_1.default.Agent({ keepAlive: true, timeout: AXIOS_TIMEOUT }) : new node_http_1.default.Agent({ keepAlive: true, timeout: AXIOS_TIMEOUT }));
this._http_session = axios_1.default.create({ timeout: AXIOS_TIMEOUT, httpAgent: this._http_agent, validateStatus: () => { return true; }, proxy: false });
}
catch (err) {
new TapoError(ErrorMsg[ErrorCode[ErrorCode.GENERIC_ERROR]] + 'Agent error - ' + err, null, ErrorCode.GENERIC_ERROR, this.constructor.name);
}
// Set the properties to false to avoid any change or access once created
if (!this._debug) {
Object.defineProperty(this, '_auth_credential', { enumerable: false });
Object.defineProperty(this, '_http_session', { enumerable: false });
Object.defineProperty(this, '_http_agent', { enumerable: false });
Object.defineProperty(this, '_session', { enumerable: false });
Object.defineProperty(this, '_jar', { enumerable: false });
Object.defineProperty(this, 'local_auth_hash', { enumerable: false });
Object.defineProperty(this, '_local_seed', { enumerable: false });
Object.defineProperty(this, '_keep_alive', { enumerable: false });
Object.defineProperty(this, '_debug', { enumerable: false });
}
}
// Private method to generate Authentication hash
generate_auth_hash(auth) {
return this._sha256(Buffer.concat([
this._sha1(Buffer.from(new node_util_1.TextEncoder().encode(auth.username))),
this._sha1(Buffer.from(new node_util_1.TextEncoder().encode(auth.password)))
]));
}
_sha1(payload) {
return (0, node_crypto_1.createHash)("sha1").update(payload).digest();
}
_sha256(payload) {
return (0, node_crypto_1.createHash)("sha256").update(payload).digest();
}
// Private method to post a session
async session_post(url, data, cookies, params) {
this._jar = null;
const config = { url: url, method: 'post', data: data, headers: (typeof (cookies) == 'undefined' ? undefined : { 'Cookie': cookies }), params: (typeof (params) == 'undefined' ? undefined : params), responseType: 'arraybuffer' };
const response = await this._http_session.request(config)
.then((value) => {
// Check status of the answer
if (value.status != 200) {
this._http_agent.destroy();
throw new TapoError(ErrorMsg[ErrorCode[ErrorCode.ERROR_aSP_bAX_REQ_FORBID]] + 'URL error - ' + value.statusText + " - " + value.status, null, ErrorCode.ERROR_aSP_bAX_REQ_FORBID, this.session_post.name);
}
else {
// Get the cookies
if (value.headers.hasOwnProperty('set-cookie')) {
const cookies = value.headers['set-cookie'][0].split(';');
for (let i = 0; i < cookies.length; i++) {
if (cookies[i].includes('=')) {
this._jar = { ...this._jar, [cookies[i].split('=')[0]]: cookies[i].split('=')[1] };
}
}
}
// Return value
return value;
}
})
.catch((error) => {
this._http_agent.destroy();
throw new TapoError(ErrorMsg[ErrorCode[ErrorCode.ERROR_aSP_bAX_REQ_ERR]] + 'Session_post error - Axios', new TapoError().axios_to_tapo(error), ErrorCode.ERROR_aSP_bAX_REQ_ERR, this.session_post.name);
});
// Build and return the array of response
const empty = JSON.stringify({});
return [response, (((response.data != null) && (typeof (response.data) != 'undefined')) ? Buffer.from(response.data) : Buffer.from(empty))];
}
// Private method Handshake to perform the full Handshake and get a valid KlapSession
async perform_handshake(new_local_seed) {
if (this._debug)
console.debug("[KLAP] Starting handshake with " + this._host);
const seeds = await this.perform_handshake1((typeof (new_local_seed) == 'undefined' ? undefined : new_local_seed))
.then(async (value) => {
const [remote_seed, auth_hash] = value;
const session = await this.perform_handshake2(this._local_seed, remote_seed, auth_hash)
.then((value) => {
if (this._debug)
console.debug("[KLAP] Handshake with " + this._host + " complete");
return value;
});
return session;
})
.catch((error) => {
this.close(false);
throw new TapoError(ErrorMsg[ErrorCode[ErrorCode.ERROR_aPH_bPH1_HSK_ERROR]] + "Handshake error - " + error.message, error, ErrorCode.ERROR_aPH_bPH1_HSK_ERROR, this.perform_handshake.name);
});
return seeds;
}
// Private method Handshake1 to get remote_seed and auth_hash
async perform_handshake1(new_local_seed) {
// Set local seed as random 16 bytes seed if not provided
this._local_seed = (typeof (new_local_seed) == 'undefined' ? Buffer.from(node_crypto_1.webcrypto.getRandomValues(new Uint8Array(16))) : new_local_seed);
// Prepare post parameters
this._session = null;
const url = this._base_url + "/handshake1";
// Send the request and check if servers answers with HTTP200
const seeds = await this.session_post(url, this._local_seed)
.then((value) => {
if (value[0].status != 200) {
this._http_agent.destroy();
throw new TapoError(ErrorMsg[ErrorCode[ErrorCode.ERROR_aPH1_bSP_HSK_FORBID]] + "Device failed to respond to handshake1 with " + value[0].status, null, ErrorCode.ERROR_aPH1_bSP_HSK_FORBID, this.perform_handshake1.name);
}
else {
// Define a new KlapSession object for the session and retrieve parameters - Session_id, Timeout, remote_seed and server_hash
const terminal_uuid = (this._terminal_random ? node_crypto_1.webcrypto.randomUUID() : (0, uuid_1.v4)());
this._session = new KlapSession(this._jar[TP_SESSION_COOKIE_NAME], parseInt(this._jar["TIMEOUT"], 10), false, terminal_uuid);
const remote_seed = value[1].subarray(0, 16);
const server_hash = value[1].subarray(16);
// Information for debugging
if (this._debug)
console.debug("Handshake1 posted " + new Date().getTime() + ". Host is " + this._host + ", Session cookie is " + this._session.session_id + ", Response status is " + value[0].status + ", Request was " + this.local_auth_hash.toString('hex'));
if (this._debug)
console.debug("Server remote_seed is " + remote_seed.toString('hex') + ", server hash is " + server_hash.toString('hex'));
// Build the local seed auth hash
const local_seed_auth_hash = this._sha256(Buffer.from(new Uint8Array([...this._local_seed, ...remote_seed, ...this.local_auth_hash])));
// Check the locally generated hash with the server one
if (Buffer.compare(local_seed_auth_hash, server_hash) == 0) {
if (this._debug)
console.debug("Handshake1 hashes matched");
return [remote_seed, this.local_auth_hash];
}
else {
// Check blank auth
if (this._debug)
console.debug("Expected " + local_seed_auth_hash.toString('hex') + " got " + server_hash.toString('hex') + " in handshake1. Checking if blank auth is a match");
const blank_auth = new AuthCredential();
const blank_auth_hash = this.generate_auth_hash(blank_auth);
const blank_seed_auth_hash = this._sha256(Buffer.from(new Uint8Array([...this._local_seed, ...remote_seed, ...blank_auth_hash])));
if (Buffer.compare(blank_seed_auth_hash, server_hash) == 0) {
if (this._debug)
console.debug("Server response doesn't match our expected hash on ip " + this._host + " but an authentication with blank credentials matched");
return [remote_seed, blank_auth_hash];
}
else {
// Check kasa setup auth
const kasa_setup_auth = new AuthCredential(TP_TEST_USER, TP_TEST_PASSWORD);
const kasa_setup_auth_hash = this.generate_auth_hash(kasa_setup_auth);
const kasa_setup_seed_auth_hash = this._sha256(Buffer.from(new Uint8Array([...this._local_seed, ...remote_seed, .