@lumiastream/tapo-cove 3.24.0 → 3.24.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +17 -9
- package/dist/index.d.ts +17 -9
- package/dist/index.js +14 -13
- package/dist/index.mjs +7 -7
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
declare const discover: (config: {
|
|
4
|
-
token?: string;
|
|
5
|
-
email?: string;
|
|
6
|
-
password?: string;
|
|
7
|
-
types?: ILumiaDeviceType[];
|
|
8
|
-
}) => Promise<any>;
|
|
1
|
+
import { ILumiaDeviceGenericRoot, ILumiaDeviceType } from '@lumiastream/lumia-rgb-types';
|
|
9
2
|
|
|
10
3
|
declare class SuperState {
|
|
11
4
|
protected _values: Record<string, boolean | string | number>;
|
|
@@ -113,6 +106,21 @@ declare class TapoApi {
|
|
|
113
106
|
getSessionData(deviceIp: string): any;
|
|
114
107
|
}
|
|
115
108
|
|
|
109
|
+
declare const discoverLocalDevices: (credentials: {
|
|
110
|
+
email: string;
|
|
111
|
+
password: string;
|
|
112
|
+
}) => Promise<{
|
|
113
|
+
ip: string;
|
|
114
|
+
mac: string;
|
|
115
|
+
loginDevice: () => Promise<TapoApi>;
|
|
116
|
+
}[]>;
|
|
117
|
+
declare const discover: (config: {
|
|
118
|
+
token?: string;
|
|
119
|
+
email?: string;
|
|
120
|
+
password?: string;
|
|
121
|
+
types?: ILumiaDeviceType[];
|
|
122
|
+
}) => Promise<any>;
|
|
123
|
+
|
|
116
124
|
declare class KlapCipher {
|
|
117
125
|
private readonly key;
|
|
118
126
|
private readonly sig;
|
|
@@ -231,4 +239,4 @@ declare namespace tapo_constants {
|
|
|
231
239
|
};
|
|
232
240
|
}
|
|
233
241
|
|
|
234
|
-
export { LightState, TapoApi, TapoCloudApi, tapo_constants as TapoConstants, discover };
|
|
242
|
+
export { LightState, TapoApi, TapoCloudApi, tapo_constants as TapoConstants, discover, discoverLocalDevices };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
declare const discover: (config: {
|
|
4
|
-
token?: string;
|
|
5
|
-
email?: string;
|
|
6
|
-
password?: string;
|
|
7
|
-
types?: ILumiaDeviceType[];
|
|
8
|
-
}) => Promise<any>;
|
|
1
|
+
import { ILumiaDeviceGenericRoot, ILumiaDeviceType } from '@lumiastream/lumia-rgb-types';
|
|
9
2
|
|
|
10
3
|
declare class SuperState {
|
|
11
4
|
protected _values: Record<string, boolean | string | number>;
|
|
@@ -113,6 +106,21 @@ declare class TapoApi {
|
|
|
113
106
|
getSessionData(deviceIp: string): any;
|
|
114
107
|
}
|
|
115
108
|
|
|
109
|
+
declare const discoverLocalDevices: (credentials: {
|
|
110
|
+
email: string;
|
|
111
|
+
password: string;
|
|
112
|
+
}) => Promise<{
|
|
113
|
+
ip: string;
|
|
114
|
+
mac: string;
|
|
115
|
+
loginDevice: () => Promise<TapoApi>;
|
|
116
|
+
}[]>;
|
|
117
|
+
declare const discover: (config: {
|
|
118
|
+
token?: string;
|
|
119
|
+
email?: string;
|
|
120
|
+
password?: string;
|
|
121
|
+
types?: ILumiaDeviceType[];
|
|
122
|
+
}) => Promise<any>;
|
|
123
|
+
|
|
116
124
|
declare class KlapCipher {
|
|
117
125
|
private readonly key;
|
|
118
126
|
private readonly sig;
|
|
@@ -231,4 +239,4 @@ declare namespace tapo_constants {
|
|
|
231
239
|
};
|
|
232
240
|
}
|
|
233
241
|
|
|
234
|
-
export { LightState, TapoApi, TapoCloudApi, tapo_constants as TapoConstants, discover };
|
|
242
|
+
export { LightState, TapoApi, TapoCloudApi, tapo_constants as TapoConstants, discover, discoverLocalDevices };
|
package/dist/index.js
CHANGED
|
@@ -2,28 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
var lumiaRgbTypes = require('@lumiastream/lumia-rgb-types');
|
|
4
4
|
var C = require('axios');
|
|
5
|
-
var
|
|
5
|
+
var se = require('local-devices');
|
|
6
6
|
var y = require('crypto');
|
|
7
|
-
var
|
|
7
|
+
var Pe = require('util');
|
|
8
8
|
var lumiaRgbUtils = require('@lumiastream/lumia-rgb-utils');
|
|
9
|
-
var
|
|
9
|
+
var Le = require('http');
|
|
10
10
|
|
|
11
11
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
12
|
|
|
13
13
|
var C__default = /*#__PURE__*/_interopDefault(C);
|
|
14
|
-
var
|
|
14
|
+
var se__default = /*#__PURE__*/_interopDefault(se);
|
|
15
15
|
var y__default = /*#__PURE__*/_interopDefault(y);
|
|
16
|
-
var
|
|
17
|
-
var
|
|
16
|
+
var Pe__default = /*#__PURE__*/_interopDefault(Pe);
|
|
17
|
+
var Le__default = /*#__PURE__*/_interopDefault(Le);
|
|
18
18
|
|
|
19
|
-
var
|
|
19
|
+
var ae=Object.defineProperty;var ne=(i,e)=>{for(var t in e)ae(i,t,{get:e[t],enumerable:!0});};var I=i=>Buffer.from(i,"base64").toString();var g=i=>{let e=i.error_code;if(e)switch(console.debug("[Tapo] errorCode: ",e),e){case 0:return;case-1010:throw new Error("Invalid public key length");case-1012:throw new Error("Invalid terminal UUID");case-1501:throw new Error("Invalid request or credentials");case-1002:throw new Error("Incorrect request");case-1003:throw new Error("JSON format error");case-20601:throw new Error("Incorrect email or password");case-20675:throw new Error("Cloud token expired or invalid");case 9999:throw new Error("Device token expired or invalid");default:throw new Error(`Unexpected Error Code: ${e} (${i.msg})`)}};var x=class{constructor(){this._values={};this.transition=e=>(this._values.transition_period=e,this);this.duration=this.transition;this.getValues=()=>this._values;}},w=class extends x{constructor(t){super();this.create=t=>(t&&Object.keys(t).forEach(s=>{let r;this.hasOwnProperty(s)&&(r=this[s],lumiaRgbUtils.isFunction(r)&&lumiaRgbUtils.isTruly(t[s])&&r.apply(this,[t[s]]));}),this);this.on=(t=!0)=>(this._values.device_on=t,this);this.turnOn=this.on;this.off=()=>(this.on(!1),this);this.turnOff=this.off;this.mode=(t="normal")=>(t==="normal"&&(this._values.color_temp=0),this._values.mode=t,this);this.hue=t=>(this._values.hue=t,this._values.color_temp=0,this);this.bri=t=>(this._values.brightness=t,this);this.brightness=this.bri;this.sat=t=>(this._values.saturation=t,this);this.saturation=this.sat;this.temp=t=>(this._values.color_temp=t,this);this.colorTemperature=this.temp;this.hsv=t=>{let s=[];return Array.isArray(t)?s=t:s=[t.h,t.s,t.v],this.hue(s[0]),this.sat(s[1]),this.bri(s[2]),this};this.rgb=t=>{let s;return Array.isArray(t)?s=lumiaRgbUtils.rgb2hsv(t):s=lumiaRgbUtils.rgb2hsv([t.r,t.g,t.b]),this.hsv(s)};this.color=t=>(Array.isArray(t)&&(t={r:t[0],g:t[1],b:t[2]}),t.ct?this.temp(t.ct):this.rgb({r:t.r,g:t.g,b:t.b}));t&&this.create(t);}};var $="aes-128-cbc",F=async(i,e,t)=>{let s=y.randomBytes(16),r=await C__default.default.post(`http://${t}/app/handshake1`,s,{responseType:"arraybuffer",withCredentials:!0,timeout:4e3}).catch(h=>{throw h?.response?.status===404?new Error("Klap protocol not supported"):new Error(`handshake1 failed: ${h}`)}),o=Buffer.from(r.data),a=r.headers["set-cookie"]?.[0],n=a?.substring(0,a.indexOf(";"))||"",c=o.slice(0,16),u=o.slice(16),p=ge(i,e),d=le(s,c,p);if(!ke(d,u))throw new Error("email or password incorrect");let l=me(s,c,p),f={};return f.Cookie=n,await C__default.default.post(`http://${t}/app/handshake2`,l,{responseType:"arraybuffer",headers:f,timeout:4e3}).catch(h=>{throw new Error(`handshake2 failed: ${h}`)}),de(t,s,c,p,n)},de=(i,e,t,s,r)=>{let o=ye(e,t,s),a=we(e,t,s),n=ve(e,t,s),c=be(a),u=f=>{let h=JSON.stringify(f),m=y.createCipheriv($,o,O(a,c));var B=m.update(A(h));return Buffer.concat([B,m.final()])},p=f=>{let h=y.createDecipheriv($,o,O(a,c));var m=h.update(f.slice(32));return JSON.parse(Buffer.concat([m,h.final()]).toString())},d=f=>{let h=u(f),m=v(Buffer.concat([n,c,h]));return Buffer.concat([m,h])};return {send:async f=>{c=Be(c);let h=d(f),m={};m.Cookie=r;let B=await C__default.default({method:"post",url:`http://${i}/app/request`,data:h,responseType:"arraybuffer",headers:m,timeout:4e3,params:{seq:c.readInt32BE()}}),_=p(B.data);return g(_),_.result},sessionCookie:r,cipher:{key:o,iv:a,seq:c}}},le=(i,e,t)=>v(Buffer.concat([i,e,t])),me=(i,e,t)=>v(Buffer.concat([e,i,t])),ge=(i,e)=>v(Buffer.concat([M(i),M(e)])),ye=(i,e,t)=>v(Buffer.concat([A("lsk"),i,e,t])).slice(0,16),we=(i,e,t)=>v(Buffer.concat([A("iv"),i,e,t])),ve=(i,e,t)=>v(Buffer.concat([A("ldk"),i,e,t])).slice(0,28),be=i=>i.slice(i.length-4),O=(i,e)=>Buffer.concat([i.slice(0,12),e]),Be=i=>{let e=Buffer.alloc(4);return e.writeInt32BE(i.readInt32BE()+1),e},v=i=>y.createHash("sha256").update(i).digest(),M=i=>y.createHash("sha1").update(i).digest(),ke=(i,e)=>i.compare(e)===0,A=i=>Buffer.from(i,"utf-8");var Se="rsa",G="aes-128-cbc",W="top secret",b=class{constructor(e,t){this.key=e;this.iv=t;}static toBase64(e){return Buffer.from(e.normalize("NFKC"),"utf-8").toString("base64")}static encodeUsername(e){let t=y__default.default.createHash("sha1");return t.update(e.normalize("NFKC")),t.digest("hex")}static createKeyPair(){return new Promise((e,t)=>{y__default.default.generateKeyPair("rsa",{modulusLength:1024},(s,r,o)=>{if(s)return t(s);let a=r.export({format:"pem",type:"spki"}).toString("base64"),n=o.export({format:"pem",type:"pkcs1"}).toString("base64");e({public:a,private:n});});})}encrypt(e){let t=y__default.default.createCipheriv("aes-128-cbc",this.key,this.iv);return `${t.update(e,"utf8","base64")}${t.final("base64")}`}decrypt(e){let t=y__default.default.createDecipheriv("aes-128-cbc",this.key,this.iv);return `${t.update(e,"base64","utf8")}${t.final("utf8")}`}},z=async()=>{let i={modulusLength:1024,publicKeyEncoding:{type:"spki",format:"pem"},privateKeyEncoding:{type:"pkcs1",format:"pem",cipher:"aes-256-cbc",passphrase:W}};return Pe__default.default.promisify(y__default.default.generateKeyPair)(Se,i)},J=(i,e)=>{var t=y__default.default.createCipheriv(G,e.key,e.iv),s=t.update(Buffer.from(JSON.stringify(i)));return Buffer.concat([s,t.final()]).toString("base64")},V=(i,e)=>{var t=y__default.default.createDecipheriv(G,e.key,e.iv),s=t.update(Buffer.from(i,"base64"));return JSON.parse(Buffer.concat([s,t.final()]).toString())},j=(i,e)=>{let t=Buffer.from(i,"base64");return y__default.default.privateDecrypt({key:e,padding:y__default.default.constants.RSA_PKCS1_PADDING,passphrase:W},t)},H=i=>Buffer.from(i).toString("base64");var Z=i=>{var e=y__default.default.createHash("sha1");return e.update(i),e.digest("hex")};var Q=async(i,e,t)=>{let s=await Te(t),r={method:"login_device",params:{username:H(Z(i)),password:H(e)},requestTimeMils:0},o=await Y(r,s);return s.token=o.token,{send:n=>Y(n,s),deviceKey:s}},Te=async i=>{let e=await z(),t={method:"handshake",params:{key:e.publicKey}},s=await C__default.default({method:"post",url:`http://${i}/app`,data:t,timeout:4e3});g(s.data);let r=s.headers["set-cookie"]?.[0],o=r?.substring(0,r.indexOf(";"))||"",a=j(s.data.result.key,e.privateKey);return {key:a.subarray(0,16),iv:a.subarray(16,32),deviceIp:i,sessionCookie:o}},Y=async(i,e)=>{let s={method:"securePassthrough",params:{request:J(i,e)}},r=await C__default.default({method:"post",url:`http://${e.deviceIp}/app?token=${e.token}`,data:s,headers:{Cookie:e.sessionCookie},timeout:4e3});g(r.data);let o=V(r.data.result.response,e);return g(o),o.result};var k=class{constructor(e){this._config={rawEmail:"",rawPassword:"",timeout:1e4,httpTimeout:4e3};this._deviceSessions=new Map;this.auth=async(e,t)=>{let s=t?.email||this._config.rawEmail,r=t?.password||this._config.rawPassword;if(this._config.rawEmail=s,this._config.rawPassword=r,!s||!r)throw new Error("Email and password are required for authentication");if(!e)throw new Error("Device IP address(es) required for authentication");return this.loginDeviceByIp(e,s,r)};this.setup=async(e,t)=>{let s=e.map(o=>o.host),r=await this.auth(s,t);return e.forEach(o=>{if(r.success.includes(o.host)){let a=this._deviceSessions.get(o.host);a&&this._deviceSessions.set(o.id,a);}}),r};this.sendState=async e=>{let t=e.device.host||e.device.id;if(!t)throw new Error("Device IP or ID is required");let s=this._deviceSessions.get(t);if(!s)throw new Error(`No session found for device ${t}. Please call auth or setup first.`);let o={method:"set_device_info",params:e.state.getValues()};s?.deviceKey&&(o.terminalUUID=lumiaRgbUtils.uuidv4());try{return await s.send(o),!0}catch(a){return console.error(`Failed to send state to device ${t}:`,a),!1}};this.sendPower=async e=>this.sendState({device:e.device,state:new w({on:e.power})});this._config={...this._config,...e},this._config.rawEmail=e?.email||"",this._config.rawPassword=e?.password||"";}async loginDeviceByIp(e,t,s){let r=t||this._config.rawEmail,o=s||this._config.rawPassword;if(!r||!o)throw new Error("Email and password are required for loginDeviceByIp");let a=Array.isArray(e)?e:[e],n={success:[],failed:[]},c=a.map(async u=>{try{try{let p=await F(r,o,u);this._deviceSessions.set(u,p),console.log(`Successfully logged in to device at ${u} using KLAP protocol`),n.success.push(u);}catch(p){console.warn(`Failed to login with KLAP protocol for ${u}: ${p.message}
|
|
20
|
+
Falling back to legacy login method`);try{let d=await Q(r,o,u);this._deviceSessions.set(u,d),console.log(`Successfully logged in to device at ${u} using legacy protocol`),n.success.push(u);}catch(d){let l=`Failed to login to device at ${u}: ${d.message}`;console.error(l),n.failed.push({ip:u,error:d.message});}}}catch(p){console.error(`Unexpected error for device ${u}:`,p),n.failed.push({ip:u,error:p.message||"Unknown error"});}});if(await Promise.allSettled(c),!Array.isArray(e)&&n.failed.length>0)throw new Error(n.failed[0].error);return n}async getDeviceInfoByIp(e){let t=this._deviceSessions.get(e);if(!t){if(!(await this.auth(e,{email:this._config.rawEmail,password:this._config.rawPassword})).success.includes(e))throw new Error(`Failed to authenticate with device at ${e}. Please check your credentials.`);if(t=this._deviceSessions.get(e),!t)throw new Error(`Failed to establish session with device at ${e} after authentication.`)}let s={method:"get_device_info"};try{let r=await t.send(s),o=`Tapo Device (${e})`;return r.nickname&&(o=atob(r.nickname)),{id:r.device_id||`tapo-${e.replace(/\./g,"-")}`,name:o,address:`http://${e}`,host:e,lumiaInfo:{alias:o,identifier:r.device_id||`tapo-${e.replace(/\./g,"-")}`,serial:r.hw_id||r.mac||e,model:r.model||"Unknown Model",lumiaType:r.device_on!==void 0?lumiaRgbTypes.ILumiaDeviceType.PLUG:lumiaRgbTypes.ILumiaDeviceType.LIGHT,zonable:!1,maxZones:r.device_on!==void 0?0:1,zones:[],rgb:r.hue!==void 0||r.color_temp!==void 0,white:r.brightness!==void 0,connectionType:lumiaRgbTypes.ILumiaDeviceConnectionType.WIFI,brand:lumiaRgbTypes.ILumiaDeviceBrands.TAPO,product:r.model||"Tapo Device"},info:r}}catch(r){throw new Error(`Failed to get device info from ${e}: ${r.message}`)}}hasDeviceSession(e){return this._deviceSessions.has(e)}clearDeviceSession(e){this._deviceSessions.delete(e);}getSessionData(e){let t=this._deviceSessions.get(e);if(!t)throw new Error(`No session found for device ${e}. Please authenticate first.`);return {deviceIp:e,sessionCookie:t.sessionCookie,cipher:t.cipher,credentials:{email:this._config.rawEmail,password:this._config.rawPassword}}}};var E=class{constructor(e,t,s){let{iv:r,seq:o}=this.ivDerive(e,t,s);this.key=this.keyDerive(e,t,s),this.sig=this.sigDerive(e,t,s),this.iv=r,this.seq=o;}encrypt(e){if(this.seq+=1,typeof e=="string"&&(e=Buffer.from(e,"utf8")),!Buffer.isBuffer(e))throw new Error("msg must be a string or buffer");let t=y__default.default.createCipheriv("aes-128-cbc",this.key,this.ivSeq()),s=Buffer.concat([t.update(e),t.final()]),r=Buffer.alloc(4);r.writeInt32BE(this.seq,0);let o=y__default.default.createHash("sha256");o.update(Buffer.concat([this.sig,r,s]));let a=o.digest();return {encrypted:Buffer.concat([a,s]),seq:this.seq}}decrypt(e){if(!Buffer.isBuffer(e))return e;let t=y__default.default.createDecipheriv("aes-128-cbc",this.key,this.ivSeq());return Buffer.concat([t.update(e.subarray(32)),t.final()]).toString("utf8")}keyDerive(e,t,s){let r=Buffer.concat([Buffer.from("lsk"),e,t,s]);return y__default.default.createHash("sha256").update(r).digest().subarray(0,16)}ivDerive(e,t,s){let r=Buffer.concat([Buffer.from("iv"),e,t,s]),o=y__default.default.createHash("sha256").update(r).digest(),a=o.subarray(-4).readInt32BE(0);return {iv:o.subarray(0,12),seq:a}}sigDerive(e,t,s){let r=Buffer.concat([Buffer.from("ldk"),e,t,s]);return y__default.default.createHash("sha256").update(r).digest().subarray(0,28)}ivSeq(){let e=Buffer.alloc(4);e.writeInt32BE(this.seq,0);let t=Buffer.concat([this.iv,e]);if(t.length!==16)throw new Error("Length of iv is not 16");return t}};var S=class S{constructor(e){this._baseUrl="https://eu-wap.tplinkcloud.com/";this._config={rawEmail:"",rawPassword:"",email:"",password:"",authToken:null,timeout:1e4,httpTimeout:4e3};this._token="";this._devices=new Map;this.auth=async e=>{let t=e?.email||this._config.email,s=e?.password||this._config.password;if(!t||!s)throw new Error("Email and password are required for authentication");let r={method:"login",params:{appType:"Tapo_Ios",cloudPassword:s,cloudUserName:t,terminalUUID:lumiaRgbUtils.uuidv4()}},o=await C__default.default({method:"post",url:this._baseUrl,data:r});return g(o.data),this._token=o.data.result.token,this._token};this.setup=async(e,t)=>{let s=e.map(async o=>{let a=await this.handshake(o.host,void 0,!1,t);this._devices.set(o.id,a);}),r=[];try{r=await Promise.allSettled(s);}catch(o){console.error("tapo cloud setup err: ",o);}return {responses:r,devices:this._devices}};this.sendState=async e=>{let t=this._devices.get(e.device.id);if(!t)return Promise.resolve(!1);await this.handshake(t?.ip,t,!1);let s=e.state.getValues(),r=JSON.stringify({method:"set_device_info",params:s}),o=t.cipher.encrypt(r);if((await this.sessionPost(t.ip,"/request",o.encrypted,"arraybuffer",t.Cookie,{seq:o.seq.toString()})).status!==200)throw new Error("[KLAP] Request failed");return !0};this.sendPower=async e=>this.sendState({device:e.device,state:new w({on:e.power})});this.axiosInstance=C__default.default.create(),this.axiosInstance.defaults.timeout=e?.httpTimeout||4e3,this._config={...this._config,...e},this._config.rawEmail=this._config.email,this._config.rawPassword=this._config.password,this._config.email=b.toBase64(b.encodeUsername(this._config.email)),this._config.password=b.toBase64(this._config.password),this.terminalUUID=y__default.default.randomUUID();}async sessionPost(e,t,s,r,o,a){let n={Accept:"*/*","Content-Type":"application/octet-stream"};return o&&(process?.versions?.electron?n.BypassCookie=o:n.Cookie=o),C__default.default.post(`http://${e}/app${t}`,s,{responseType:r,params:a,headers:n,httpAgent:new Le__default.default.Agent({keepAlive:!1})})}needsNewHandshake(e){return !!(!e||!e.cipher||e.IsExpired||!e.Cookie)}async handshake(e,t,s=!1,r){if(!this.needsNewHandshake(t)&&!s)return;let{localSeed:o,remoteSeed:a,authHash:n,deviceSession:c}=await this.firstHandshake(e,void 0,r);return await this.secondHandshake(c,e,o,a,n,r)}async firstHandshake(e,t,s){let r=t||y__default.default.randomBytes(16),o=await this.sessionPost(e,"/handshake1",r,"arraybuffer");if(s?.debug("handshake1Result: ",o),o.status!==200)throw new Error("Handshake1 failed");if(o.headers["content-length"]!=="48")throw new Error("Handshake1 failed due to invalid content length");let a=o.headers["bypass-cookie"]||o.headers["set-cookie"]?.[0],n=Buffer.from(new Uint8Array(o.data)),[c,u]=a.split(";"),p=u.split("=").pop(),d=new q(p,e,c),l=n.subarray(0,16),f=n.subarray(16);s?.debug(`[KLAP] First handshake decoded successfully:
|
|
20
21
|
Remote Seed:`,l.toString("hex"),`
|
|
21
22
|
Server Hash:`,f.toString("hex"),`
|
|
22
|
-
Cookie:`,c);let h=this.hashAuth(this._config.rawEmail,this._config.rawPassword),m=this.sha256(Buffer.concat([
|
|
23
|
-
Falling back to legacy login method`);try{let d=await ee(s,o,u);this._deviceSessions.set(u,d),console.log(`Successfully logged in to device at ${u} using legacy protocol`),a.success.push(u);}catch(d){let l=`Failed to login to device at ${u}: ${d.message}`;console.error(l),a.failed.push({ip:u,error:d.message});}}}catch(p){console.error(`Unexpected error for device ${u}:`,p),a.failed.push({ip:u,error:p.message||"Unknown error"});}});if(await Promise.allSettled(c),!Array.isArray(e)&&a.failed.length>0)throw new Error(a.failed[0].error);return a}async getDeviceInfoByIp(e){let t=this._deviceSessions.get(e);if(!t){if(!(await this.auth(e,{email:this._config.rawEmail,password:this._config.rawPassword})).success.includes(e))throw new Error(`Failed to authenticate with device at ${e}. Please check your credentials.`);if(t=this._deviceSessions.get(e),!t)throw new Error(`Failed to establish session with device at ${e} after authentication.`)}let r={method:"get_device_info"};try{let s=await t.send(r),o=`Tapo Device (${e})`;return s.nickname&&(o=atob(s.nickname)),{id:s.device_id||`tapo-${e.replace(/\./g,"-")}`,name:o,address:`http://${e}`,host:e,lumiaInfo:{alias:o,identifier:s.device_id||`tapo-${e.replace(/\./g,"-")}`,serial:s.hw_id||s.mac||e,model:s.model||"Unknown Model",lumiaType:s.device_on!==void 0?lumiaRgbTypes.ILumiaDeviceType.PLUG:lumiaRgbTypes.ILumiaDeviceType.LIGHT,zonable:!1,maxZones:s.device_on!==void 0?0:1,zones:[],rgb:s.hue!==void 0||s.color_temp!==void 0,white:s.brightness!==void 0,connectionType:lumiaRgbTypes.ILumiaDeviceConnectionType.WIFI,brand:lumiaRgbTypes.ILumiaDeviceBrands.TAPO,product:s.model||"Tapo Device"},info:s}}catch(s){throw new Error(`Failed to get device info from ${e}: ${s.message}`)}}hasDeviceSession(e){return this._deviceSessions.has(e)}clearDeviceSession(e){this._deviceSessions.delete(e);}getSessionData(e){let t=this._deviceSessions.get(e);if(!t)throw new Error(`No session found for device ${e}. Please authenticate first.`);return {deviceIp:e,sessionCookie:t.sessionCookie,cipher:t.cipher,credentials:{email:this._config.rawEmail,password:this._config.rawPassword}}}};var se={};oe(se,{DeviceResTypes:()=>He,DeviceSendValues:()=>Ke,ETapoDeviceTypes:()=>re,TapoDeviceTypes:()=>xe});var xe={ALL:"all",BULBS:"bulb",LIGHTSTRIPS:"lightstrip",PLUGS:"plug"},re=(s=>(s.ALL="all",s.BULBS="bulb",s.LIGHTSTRIPS="lightstrip",s.PLUGS="plug",s))(re||{}),He={BULB:"IOT.SMARTPLUGSWITCH",PLUG:"IOT.SMARTPLUGSWITCH"},Ke={BULB:"smartlife.iot.smartbulb.lightingservice",LIGHTSTRIP:"smartlife.iot.lightStrip",PLUG:"system"};
|
|
23
|
+
Cookie:`,c);let h=this.hashAuth(this._config.rawEmail,this._config.rawPassword),m=this.sha256(Buffer.concat([r,l,h]));if(Buffer.compare(m,f)===0)return s?.debug("[KLAP] Local auth hash matches server hash"),{localSeed:r,remoteSeed:l,authHash:h,deviceSession:d};let B=this.sha256(Buffer.concat([r,l,this.hashAuth("","")]));if(Buffer.compare(B,f)===0)return s?.debug("[KLAP] [WARN] Empty auth hash matches server hash"),{localSeed:r,remoteSeed:l,authHash:B,deviceSession:d};let _=this.sha256(Buffer.concat([r,l,this.hashAuth(S.TP_TEST_USER,S.TP_TEST_PASSWORD)]));if(Buffer.compare(_,f)===0)return s?.debug("[KLAP] [WARN] Test auth hash matches server hash"),{localSeed:r,remoteSeed:l,authHash:_,deviceSession:d};throw new Error("Failed to verify server hash")}async secondHandshake(e,t,s,r,o,a){let n=this.sha256(Buffer.concat([r,s,o]));try{let c=await this.sessionPost(t,"/handshake2",n,"text",e.Cookie);if(c.status===200)return a?.debug("[KLAP] Second handshake successful"),e.completeHandshake(t,new E(s,r,o));a.warn("[KLAP] Second handshake failed",c.data);}catch(c){a.error("[KLAP] Second handshake failed:",c.response.data||c.message);}}sha256(e){return y__default.default.createHash("sha256").update(e).digest()}sha1(e){return y__default.default.createHash("sha1").update(e).digest()}hashAuth(e,t){return this.sha256(Buffer.concat([this.sha1(Buffer.from(e.normalize("NFKC"))),this.sha1(Buffer.from(t.normalize("NFKC")))]))}};S.TP_TEST_USER="test@tp-link.net",S.TP_TEST_PASSWORD="test";var T=S,q=class i{constructor(e,t,s,r){this.ip=t;this.cookie=s;this.cipher=r;this.handshakeCompleted=!1;this.rawTimeout=e,this.expireAt=new Date(Date.now()+parseInt(e)*1e3),r&&(this.handshakeCompleted=!0);}get IsExpired(){return this.expireAt.getTime()-Date.now()<=40*1e3}get Cookie(){return this.cookie}completeHandshake(e,t){return new i(this.rawTimeout,e,this.cookie,t)}};var Ie=["84:d8:1b","78:8c:b5","cc:ba:bd","e4:fa:c4","ac:84:c6","50:c7:bf"],xe=5,re=i=>(i||"").replace(/:/g,"").toLowerCase(),Ce=i=>{let e=(i||"").toLowerCase();return Ie.some(t=>e.startsWith(t))},He=async i=>{let e=re(i),t=0;for(;t<=xe;){let r=(await se__default.default({skipNameResolution:!0})).find(o=>re(o.mac)===e);if(r?.ip)return r.ip;t++;}},Ke=async i=>{let e=await se__default.default({skipNameResolution:!0}),{email:t,password:s}=i;return e.filter(r=>Ce(r.mac)).map(r=>({ip:r.ip,mac:r.mac,loginDevice:async()=>{let o=new k({email:t,password:s}),a=await o.auth(r.ip,{email:t,password:s});if(!a.success.includes(r.ip)){let n=a.failed.find(c=>c.ip===r.ip);throw new Error(n?.error||`Failed to authenticate with device ${r.ip}`)}return o}}))},qe=async i=>{let e=new T(i),t=i.token;!t&&i.email&&i.password&&(t=await e.auth({email:i.email,password:i.password}));let s={method:"getDeviceList"},r=await C__default.default({method:"post",url:`${e._baseUrl}?token=${t}`,data:s});g(r.data);let o=[];for(let a of r.data.result?.deviceList||[]){if(!a.ip){let n=await He(a.deviceMac);if(!n)continue;a.ip=n;}switch(a.deviceType){case"IOT.SMARTBULB":case"SMART.TAPOBULB":{if(i.types&&!i.types.includes(lumiaRgbTypes.ILumiaDeviceType.LIGHT))return;let n=a.deviceType==="SMART.TAPOBULB",c=(n?I(a.alias):a.alias)??a.deviceName;o.push({name:c,id:a.deviceMac,address:`http://${a.ip}`,host:a.ip,lumiaInfo:{alias:c,identifier:a.deviceMac,serial:a.hwId,lumiaType:lumiaRgbTypes.ILumiaDeviceType.LIGHT,zonable:!1,maxZones:1,zones:[],rgb:!0,white:!0,connectionType:lumiaRgbTypes.ILumiaDeviceConnectionType.WIFI,brand:n?lumiaRgbTypes.ILumiaDeviceBrands.TPLINK:lumiaRgbTypes.ILumiaDeviceBrands.TPLINK,product:a?.deviceModel}});break}case"IOT.SMARTPLUGSWITCH":case"SMART.TAPOPLUG":{if(i.types&&!i.types.includes(lumiaRgbTypes.ILumiaDeviceType.PLUG))return;let n=a.deviceType==="SMART.TAPOPLUG",c=(n?I(a.alias):a.alias)??a.deviceName;o.push({name:c,id:a.deviceMac,address:`http://${a.ip}`,host:a.ip,lumiaInfo:{alias:c,identifier:a.deviceMac,serial:a.hwId,lumiaType:lumiaRgbTypes.ILumiaDeviceType.PLUG,zonable:!1,maxZones:0,zones:[],rgb:!1,white:!1,connectionType:lumiaRgbTypes.ILumiaDeviceConnectionType.WIFI,brand:n?lumiaRgbTypes.ILumiaDeviceBrands.TPLINK:lumiaRgbTypes.ILumiaDeviceBrands.TPLINK,product:a?.deviceModel}});break}}}return o},Ue=qe;var oe={};ne(oe,{DeviceResTypes:()=>Oe,DeviceSendValues:()=>Me,ETapoDeviceTypes:()=>ie,TapoDeviceTypes:()=>$e});var $e={ALL:"all",BULBS:"bulb",LIGHTSTRIPS:"lightstrip",PLUGS:"plug"},ie=(r=>(r.ALL="all",r.BULBS="bulb",r.LIGHTSTRIPS="lightstrip",r.PLUGS="plug",r))(ie||{}),Oe={BULB:"IOT.SMARTPLUGSWITCH",PLUG:"IOT.SMARTPLUGSWITCH"},Me={BULB:"smartlife.iot.smartbulb.lightingservice",LIGHTSTRIP:"smartlife.iot.lightStrip",PLUG:"system"};
|
|
24
24
|
|
|
25
25
|
exports.LightState = w;
|
|
26
|
-
exports.TapoApi =
|
|
27
|
-
exports.TapoCloudApi =
|
|
28
|
-
exports.TapoConstants =
|
|
29
|
-
exports.discover =
|
|
26
|
+
exports.TapoApi = k;
|
|
27
|
+
exports.TapoCloudApi = T;
|
|
28
|
+
exports.TapoConstants = oe;
|
|
29
|
+
exports.discover = Ue;
|
|
30
|
+
exports.discoverLocalDevices = Ke;
|
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { ILumiaDeviceType, ILumiaDeviceConnectionType, ILumiaDeviceBrands } from '@lumiastream/lumia-rgb-types';
|
|
2
2
|
import C from 'axios';
|
|
3
|
-
import
|
|
3
|
+
import se from 'local-devices';
|
|
4
4
|
import y, { randomBytes, createHash, createDecipheriv, createCipheriv } from 'crypto';
|
|
5
|
-
import
|
|
5
|
+
import Pe from 'util';
|
|
6
6
|
import { isFunction, isTruly, rgb2hsv, uuidv4 } from '@lumiastream/lumia-rgb-utils';
|
|
7
|
-
import
|
|
7
|
+
import Le from 'http';
|
|
8
8
|
|
|
9
|
-
var
|
|
9
|
+
var ae=Object.defineProperty;var ne=(i,e)=>{for(var t in e)ae(i,t,{get:e[t],enumerable:!0});};var I=i=>Buffer.from(i,"base64").toString();var g=i=>{let e=i.error_code;if(e)switch(console.debug("[Tapo] errorCode: ",e),e){case 0:return;case-1010:throw new Error("Invalid public key length");case-1012:throw new Error("Invalid terminal UUID");case-1501:throw new Error("Invalid request or credentials");case-1002:throw new Error("Incorrect request");case-1003:throw new Error("JSON format error");case-20601:throw new Error("Incorrect email or password");case-20675:throw new Error("Cloud token expired or invalid");case 9999:throw new Error("Device token expired or invalid");default:throw new Error(`Unexpected Error Code: ${e} (${i.msg})`)}};var x=class{constructor(){this._values={};this.transition=e=>(this._values.transition_period=e,this);this.duration=this.transition;this.getValues=()=>this._values;}},w=class extends x{constructor(t){super();this.create=t=>(t&&Object.keys(t).forEach(s=>{let r;this.hasOwnProperty(s)&&(r=this[s],isFunction(r)&&isTruly(t[s])&&r.apply(this,[t[s]]));}),this);this.on=(t=!0)=>(this._values.device_on=t,this);this.turnOn=this.on;this.off=()=>(this.on(!1),this);this.turnOff=this.off;this.mode=(t="normal")=>(t==="normal"&&(this._values.color_temp=0),this._values.mode=t,this);this.hue=t=>(this._values.hue=t,this._values.color_temp=0,this);this.bri=t=>(this._values.brightness=t,this);this.brightness=this.bri;this.sat=t=>(this._values.saturation=t,this);this.saturation=this.sat;this.temp=t=>(this._values.color_temp=t,this);this.colorTemperature=this.temp;this.hsv=t=>{let s=[];return Array.isArray(t)?s=t:s=[t.h,t.s,t.v],this.hue(s[0]),this.sat(s[1]),this.bri(s[2]),this};this.rgb=t=>{let s;return Array.isArray(t)?s=rgb2hsv(t):s=rgb2hsv([t.r,t.g,t.b]),this.hsv(s)};this.color=t=>(Array.isArray(t)&&(t={r:t[0],g:t[1],b:t[2]}),t.ct?this.temp(t.ct):this.rgb({r:t.r,g:t.g,b:t.b}));t&&this.create(t);}};var $="aes-128-cbc",F=async(i,e,t)=>{let s=randomBytes(16),r=await C.post(`http://${t}/app/handshake1`,s,{responseType:"arraybuffer",withCredentials:!0,timeout:4e3}).catch(h=>{throw h?.response?.status===404?new Error("Klap protocol not supported"):new Error(`handshake1 failed: ${h}`)}),o=Buffer.from(r.data),a=r.headers["set-cookie"]?.[0],n=a?.substring(0,a.indexOf(";"))||"",c=o.slice(0,16),u=o.slice(16),p=ge(i,e),d=le(s,c,p);if(!ke(d,u))throw new Error("email or password incorrect");let l=me(s,c,p),f={};return f.Cookie=n,await C.post(`http://${t}/app/handshake2`,l,{responseType:"arraybuffer",headers:f,timeout:4e3}).catch(h=>{throw new Error(`handshake2 failed: ${h}`)}),de(t,s,c,p,n)},de=(i,e,t,s,r)=>{let o=ye(e,t,s),a=we(e,t,s),n=ve(e,t,s),c=be(a),u=f=>{let h=JSON.stringify(f),m=createCipheriv($,o,O(a,c));var B=m.update(A(h));return Buffer.concat([B,m.final()])},p=f=>{let h=createDecipheriv($,o,O(a,c));var m=h.update(f.slice(32));return JSON.parse(Buffer.concat([m,h.final()]).toString())},d=f=>{let h=u(f),m=v(Buffer.concat([n,c,h]));return Buffer.concat([m,h])};return {send:async f=>{c=Be(c);let h=d(f),m={};m.Cookie=r;let B=await C({method:"post",url:`http://${i}/app/request`,data:h,responseType:"arraybuffer",headers:m,timeout:4e3,params:{seq:c.readInt32BE()}}),_=p(B.data);return g(_),_.result},sessionCookie:r,cipher:{key:o,iv:a,seq:c}}},le=(i,e,t)=>v(Buffer.concat([i,e,t])),me=(i,e,t)=>v(Buffer.concat([e,i,t])),ge=(i,e)=>v(Buffer.concat([M(i),M(e)])),ye=(i,e,t)=>v(Buffer.concat([A("lsk"),i,e,t])).slice(0,16),we=(i,e,t)=>v(Buffer.concat([A("iv"),i,e,t])),ve=(i,e,t)=>v(Buffer.concat([A("ldk"),i,e,t])).slice(0,28),be=i=>i.slice(i.length-4),O=(i,e)=>Buffer.concat([i.slice(0,12),e]),Be=i=>{let e=Buffer.alloc(4);return e.writeInt32BE(i.readInt32BE()+1),e},v=i=>createHash("sha256").update(i).digest(),M=i=>createHash("sha1").update(i).digest(),ke=(i,e)=>i.compare(e)===0,A=i=>Buffer.from(i,"utf-8");var Se="rsa",G="aes-128-cbc",W="top secret",b=class{constructor(e,t){this.key=e;this.iv=t;}static toBase64(e){return Buffer.from(e.normalize("NFKC"),"utf-8").toString("base64")}static encodeUsername(e){let t=y.createHash("sha1");return t.update(e.normalize("NFKC")),t.digest("hex")}static createKeyPair(){return new Promise((e,t)=>{y.generateKeyPair("rsa",{modulusLength:1024},(s,r,o)=>{if(s)return t(s);let a=r.export({format:"pem",type:"spki"}).toString("base64"),n=o.export({format:"pem",type:"pkcs1"}).toString("base64");e({public:a,private:n});});})}encrypt(e){let t=y.createCipheriv("aes-128-cbc",this.key,this.iv);return `${t.update(e,"utf8","base64")}${t.final("base64")}`}decrypt(e){let t=y.createDecipheriv("aes-128-cbc",this.key,this.iv);return `${t.update(e,"base64","utf8")}${t.final("utf8")}`}},z=async()=>{let i={modulusLength:1024,publicKeyEncoding:{type:"spki",format:"pem"},privateKeyEncoding:{type:"pkcs1",format:"pem",cipher:"aes-256-cbc",passphrase:W}};return Pe.promisify(y.generateKeyPair)(Se,i)},J=(i,e)=>{var t=y.createCipheriv(G,e.key,e.iv),s=t.update(Buffer.from(JSON.stringify(i)));return Buffer.concat([s,t.final()]).toString("base64")},V=(i,e)=>{var t=y.createDecipheriv(G,e.key,e.iv),s=t.update(Buffer.from(i,"base64"));return JSON.parse(Buffer.concat([s,t.final()]).toString())},j=(i,e)=>{let t=Buffer.from(i,"base64");return y.privateDecrypt({key:e,padding:y.constants.RSA_PKCS1_PADDING,passphrase:W},t)},H=i=>Buffer.from(i).toString("base64");var Z=i=>{var e=y.createHash("sha1");return e.update(i),e.digest("hex")};var Q=async(i,e,t)=>{let s=await Te(t),r={method:"login_device",params:{username:H(Z(i)),password:H(e)},requestTimeMils:0},o=await Y(r,s);return s.token=o.token,{send:n=>Y(n,s),deviceKey:s}},Te=async i=>{let e=await z(),t={method:"handshake",params:{key:e.publicKey}},s=await C({method:"post",url:`http://${i}/app`,data:t,timeout:4e3});g(s.data);let r=s.headers["set-cookie"]?.[0],o=r?.substring(0,r.indexOf(";"))||"",a=j(s.data.result.key,e.privateKey);return {key:a.subarray(0,16),iv:a.subarray(16,32),deviceIp:i,sessionCookie:o}},Y=async(i,e)=>{let s={method:"securePassthrough",params:{request:J(i,e)}},r=await C({method:"post",url:`http://${e.deviceIp}/app?token=${e.token}`,data:s,headers:{Cookie:e.sessionCookie},timeout:4e3});g(r.data);let o=V(r.data.result.response,e);return g(o),o.result};var k=class{constructor(e){this._config={rawEmail:"",rawPassword:"",timeout:1e4,httpTimeout:4e3};this._deviceSessions=new Map;this.auth=async(e,t)=>{let s=t?.email||this._config.rawEmail,r=t?.password||this._config.rawPassword;if(this._config.rawEmail=s,this._config.rawPassword=r,!s||!r)throw new Error("Email and password are required for authentication");if(!e)throw new Error("Device IP address(es) required for authentication");return this.loginDeviceByIp(e,s,r)};this.setup=async(e,t)=>{let s=e.map(o=>o.host),r=await this.auth(s,t);return e.forEach(o=>{if(r.success.includes(o.host)){let a=this._deviceSessions.get(o.host);a&&this._deviceSessions.set(o.id,a);}}),r};this.sendState=async e=>{let t=e.device.host||e.device.id;if(!t)throw new Error("Device IP or ID is required");let s=this._deviceSessions.get(t);if(!s)throw new Error(`No session found for device ${t}. Please call auth or setup first.`);let o={method:"set_device_info",params:e.state.getValues()};s?.deviceKey&&(o.terminalUUID=uuidv4());try{return await s.send(o),!0}catch(a){return console.error(`Failed to send state to device ${t}:`,a),!1}};this.sendPower=async e=>this.sendState({device:e.device,state:new w({on:e.power})});this._config={...this._config,...e},this._config.rawEmail=e?.email||"",this._config.rawPassword=e?.password||"";}async loginDeviceByIp(e,t,s){let r=t||this._config.rawEmail,o=s||this._config.rawPassword;if(!r||!o)throw new Error("Email and password are required for loginDeviceByIp");let a=Array.isArray(e)?e:[e],n={success:[],failed:[]},c=a.map(async u=>{try{try{let p=await F(r,o,u);this._deviceSessions.set(u,p),console.log(`Successfully logged in to device at ${u} using KLAP protocol`),n.success.push(u);}catch(p){console.warn(`Failed to login with KLAP protocol for ${u}: ${p.message}
|
|
10
|
+
Falling back to legacy login method`);try{let d=await Q(r,o,u);this._deviceSessions.set(u,d),console.log(`Successfully logged in to device at ${u} using legacy protocol`),n.success.push(u);}catch(d){let l=`Failed to login to device at ${u}: ${d.message}`;console.error(l),n.failed.push({ip:u,error:d.message});}}}catch(p){console.error(`Unexpected error for device ${u}:`,p),n.failed.push({ip:u,error:p.message||"Unknown error"});}});if(await Promise.allSettled(c),!Array.isArray(e)&&n.failed.length>0)throw new Error(n.failed[0].error);return n}async getDeviceInfoByIp(e){let t=this._deviceSessions.get(e);if(!t){if(!(await this.auth(e,{email:this._config.rawEmail,password:this._config.rawPassword})).success.includes(e))throw new Error(`Failed to authenticate with device at ${e}. Please check your credentials.`);if(t=this._deviceSessions.get(e),!t)throw new Error(`Failed to establish session with device at ${e} after authentication.`)}let s={method:"get_device_info"};try{let r=await t.send(s),o=`Tapo Device (${e})`;return r.nickname&&(o=atob(r.nickname)),{id:r.device_id||`tapo-${e.replace(/\./g,"-")}`,name:o,address:`http://${e}`,host:e,lumiaInfo:{alias:o,identifier:r.device_id||`tapo-${e.replace(/\./g,"-")}`,serial:r.hw_id||r.mac||e,model:r.model||"Unknown Model",lumiaType:r.device_on!==void 0?ILumiaDeviceType.PLUG:ILumiaDeviceType.LIGHT,zonable:!1,maxZones:r.device_on!==void 0?0:1,zones:[],rgb:r.hue!==void 0||r.color_temp!==void 0,white:r.brightness!==void 0,connectionType:ILumiaDeviceConnectionType.WIFI,brand:ILumiaDeviceBrands.TAPO,product:r.model||"Tapo Device"},info:r}}catch(r){throw new Error(`Failed to get device info from ${e}: ${r.message}`)}}hasDeviceSession(e){return this._deviceSessions.has(e)}clearDeviceSession(e){this._deviceSessions.delete(e);}getSessionData(e){let t=this._deviceSessions.get(e);if(!t)throw new Error(`No session found for device ${e}. Please authenticate first.`);return {deviceIp:e,sessionCookie:t.sessionCookie,cipher:t.cipher,credentials:{email:this._config.rawEmail,password:this._config.rawPassword}}}};var E=class{constructor(e,t,s){let{iv:r,seq:o}=this.ivDerive(e,t,s);this.key=this.keyDerive(e,t,s),this.sig=this.sigDerive(e,t,s),this.iv=r,this.seq=o;}encrypt(e){if(this.seq+=1,typeof e=="string"&&(e=Buffer.from(e,"utf8")),!Buffer.isBuffer(e))throw new Error("msg must be a string or buffer");let t=y.createCipheriv("aes-128-cbc",this.key,this.ivSeq()),s=Buffer.concat([t.update(e),t.final()]),r=Buffer.alloc(4);r.writeInt32BE(this.seq,0);let o=y.createHash("sha256");o.update(Buffer.concat([this.sig,r,s]));let a=o.digest();return {encrypted:Buffer.concat([a,s]),seq:this.seq}}decrypt(e){if(!Buffer.isBuffer(e))return e;let t=y.createDecipheriv("aes-128-cbc",this.key,this.ivSeq());return Buffer.concat([t.update(e.subarray(32)),t.final()]).toString("utf8")}keyDerive(e,t,s){let r=Buffer.concat([Buffer.from("lsk"),e,t,s]);return y.createHash("sha256").update(r).digest().subarray(0,16)}ivDerive(e,t,s){let r=Buffer.concat([Buffer.from("iv"),e,t,s]),o=y.createHash("sha256").update(r).digest(),a=o.subarray(-4).readInt32BE(0);return {iv:o.subarray(0,12),seq:a}}sigDerive(e,t,s){let r=Buffer.concat([Buffer.from("ldk"),e,t,s]);return y.createHash("sha256").update(r).digest().subarray(0,28)}ivSeq(){let e=Buffer.alloc(4);e.writeInt32BE(this.seq,0);let t=Buffer.concat([this.iv,e]);if(t.length!==16)throw new Error("Length of iv is not 16");return t}};var S=class S{constructor(e){this._baseUrl="https://eu-wap.tplinkcloud.com/";this._config={rawEmail:"",rawPassword:"",email:"",password:"",authToken:null,timeout:1e4,httpTimeout:4e3};this._token="";this._devices=new Map;this.auth=async e=>{let t=e?.email||this._config.email,s=e?.password||this._config.password;if(!t||!s)throw new Error("Email and password are required for authentication");let r={method:"login",params:{appType:"Tapo_Ios",cloudPassword:s,cloudUserName:t,terminalUUID:uuidv4()}},o=await C({method:"post",url:this._baseUrl,data:r});return g(o.data),this._token=o.data.result.token,this._token};this.setup=async(e,t)=>{let s=e.map(async o=>{let a=await this.handshake(o.host,void 0,!1,t);this._devices.set(o.id,a);}),r=[];try{r=await Promise.allSettled(s);}catch(o){console.error("tapo cloud setup err: ",o);}return {responses:r,devices:this._devices}};this.sendState=async e=>{let t=this._devices.get(e.device.id);if(!t)return Promise.resolve(!1);await this.handshake(t?.ip,t,!1);let s=e.state.getValues(),r=JSON.stringify({method:"set_device_info",params:s}),o=t.cipher.encrypt(r);if((await this.sessionPost(t.ip,"/request",o.encrypted,"arraybuffer",t.Cookie,{seq:o.seq.toString()})).status!==200)throw new Error("[KLAP] Request failed");return !0};this.sendPower=async e=>this.sendState({device:e.device,state:new w({on:e.power})});this.axiosInstance=C.create(),this.axiosInstance.defaults.timeout=e?.httpTimeout||4e3,this._config={...this._config,...e},this._config.rawEmail=this._config.email,this._config.rawPassword=this._config.password,this._config.email=b.toBase64(b.encodeUsername(this._config.email)),this._config.password=b.toBase64(this._config.password),this.terminalUUID=y.randomUUID();}async sessionPost(e,t,s,r,o,a){let n={Accept:"*/*","Content-Type":"application/octet-stream"};return o&&(process?.versions?.electron?n.BypassCookie=o:n.Cookie=o),C.post(`http://${e}/app${t}`,s,{responseType:r,params:a,headers:n,httpAgent:new Le.Agent({keepAlive:!1})})}needsNewHandshake(e){return !!(!e||!e.cipher||e.IsExpired||!e.Cookie)}async handshake(e,t,s=!1,r){if(!this.needsNewHandshake(t)&&!s)return;let{localSeed:o,remoteSeed:a,authHash:n,deviceSession:c}=await this.firstHandshake(e,void 0,r);return await this.secondHandshake(c,e,o,a,n,r)}async firstHandshake(e,t,s){let r=t||y.randomBytes(16),o=await this.sessionPost(e,"/handshake1",r,"arraybuffer");if(s?.debug("handshake1Result: ",o),o.status!==200)throw new Error("Handshake1 failed");if(o.headers["content-length"]!=="48")throw new Error("Handshake1 failed due to invalid content length");let a=o.headers["bypass-cookie"]||o.headers["set-cookie"]?.[0],n=Buffer.from(new Uint8Array(o.data)),[c,u]=a.split(";"),p=u.split("=").pop(),d=new q(p,e,c),l=n.subarray(0,16),f=n.subarray(16);s?.debug(`[KLAP] First handshake decoded successfully:
|
|
10
11
|
Remote Seed:`,l.toString("hex"),`
|
|
11
12
|
Server Hash:`,f.toString("hex"),`
|
|
12
|
-
Cookie:`,c);let h=this.hashAuth(this._config.rawEmail,this._config.rawPassword),m=this.sha256(Buffer.concat([
|
|
13
|
-
Falling back to legacy login method`);try{let d=await ee(s,o,u);this._deviceSessions.set(u,d),console.log(`Successfully logged in to device at ${u} using legacy protocol`),a.success.push(u);}catch(d){let l=`Failed to login to device at ${u}: ${d.message}`;console.error(l),a.failed.push({ip:u,error:d.message});}}}catch(p){console.error(`Unexpected error for device ${u}:`,p),a.failed.push({ip:u,error:p.message||"Unknown error"});}});if(await Promise.allSettled(c),!Array.isArray(e)&&a.failed.length>0)throw new Error(a.failed[0].error);return a}async getDeviceInfoByIp(e){let t=this._deviceSessions.get(e);if(!t){if(!(await this.auth(e,{email:this._config.rawEmail,password:this._config.rawPassword})).success.includes(e))throw new Error(`Failed to authenticate with device at ${e}. Please check your credentials.`);if(t=this._deviceSessions.get(e),!t)throw new Error(`Failed to establish session with device at ${e} after authentication.`)}let r={method:"get_device_info"};try{let s=await t.send(r),o=`Tapo Device (${e})`;return s.nickname&&(o=atob(s.nickname)),{id:s.device_id||`tapo-${e.replace(/\./g,"-")}`,name:o,address:`http://${e}`,host:e,lumiaInfo:{alias:o,identifier:s.device_id||`tapo-${e.replace(/\./g,"-")}`,serial:s.hw_id||s.mac||e,model:s.model||"Unknown Model",lumiaType:s.device_on!==void 0?ILumiaDeviceType.PLUG:ILumiaDeviceType.LIGHT,zonable:!1,maxZones:s.device_on!==void 0?0:1,zones:[],rgb:s.hue!==void 0||s.color_temp!==void 0,white:s.brightness!==void 0,connectionType:ILumiaDeviceConnectionType.WIFI,brand:ILumiaDeviceBrands.TAPO,product:s.model||"Tapo Device"},info:s}}catch(s){throw new Error(`Failed to get device info from ${e}: ${s.message}`)}}hasDeviceSession(e){return this._deviceSessions.has(e)}clearDeviceSession(e){this._deviceSessions.delete(e);}getSessionData(e){let t=this._deviceSessions.get(e);if(!t)throw new Error(`No session found for device ${e}. Please authenticate first.`);return {deviceIp:e,sessionCookie:t.sessionCookie,cipher:t.cipher,credentials:{email:this._config.rawEmail,password:this._config.rawPassword}}}};var se={};oe(se,{DeviceResTypes:()=>He,DeviceSendValues:()=>Ke,ETapoDeviceTypes:()=>re,TapoDeviceTypes:()=>xe});var xe={ALL:"all",BULBS:"bulb",LIGHTSTRIPS:"lightstrip",PLUGS:"plug"},re=(s=>(s.ALL="all",s.BULBS="bulb",s.LIGHTSTRIPS="lightstrip",s.PLUGS="plug",s))(re||{}),He={BULB:"IOT.SMARTPLUGSWITCH",PLUG:"IOT.SMARTPLUGSWITCH"},Ke={BULB:"smartlife.iot.smartbulb.lightingservice",LIGHTSTRIP:"smartlife.iot.lightStrip",PLUG:"system"};
|
|
13
|
+
Cookie:`,c);let h=this.hashAuth(this._config.rawEmail,this._config.rawPassword),m=this.sha256(Buffer.concat([r,l,h]));if(Buffer.compare(m,f)===0)return s?.debug("[KLAP] Local auth hash matches server hash"),{localSeed:r,remoteSeed:l,authHash:h,deviceSession:d};let B=this.sha256(Buffer.concat([r,l,this.hashAuth("","")]));if(Buffer.compare(B,f)===0)return s?.debug("[KLAP] [WARN] Empty auth hash matches server hash"),{localSeed:r,remoteSeed:l,authHash:B,deviceSession:d};let _=this.sha256(Buffer.concat([r,l,this.hashAuth(S.TP_TEST_USER,S.TP_TEST_PASSWORD)]));if(Buffer.compare(_,f)===0)return s?.debug("[KLAP] [WARN] Test auth hash matches server hash"),{localSeed:r,remoteSeed:l,authHash:_,deviceSession:d};throw new Error("Failed to verify server hash")}async secondHandshake(e,t,s,r,o,a){let n=this.sha256(Buffer.concat([r,s,o]));try{let c=await this.sessionPost(t,"/handshake2",n,"text",e.Cookie);if(c.status===200)return a?.debug("[KLAP] Second handshake successful"),e.completeHandshake(t,new E(s,r,o));a.warn("[KLAP] Second handshake failed",c.data);}catch(c){a.error("[KLAP] Second handshake failed:",c.response.data||c.message);}}sha256(e){return y.createHash("sha256").update(e).digest()}sha1(e){return y.createHash("sha1").update(e).digest()}hashAuth(e,t){return this.sha256(Buffer.concat([this.sha1(Buffer.from(e.normalize("NFKC"))),this.sha1(Buffer.from(t.normalize("NFKC")))]))}};S.TP_TEST_USER="test@tp-link.net",S.TP_TEST_PASSWORD="test";var T=S,q=class i{constructor(e,t,s,r){this.ip=t;this.cookie=s;this.cipher=r;this.handshakeCompleted=!1;this.rawTimeout=e,this.expireAt=new Date(Date.now()+parseInt(e)*1e3),r&&(this.handshakeCompleted=!0);}get IsExpired(){return this.expireAt.getTime()-Date.now()<=40*1e3}get Cookie(){return this.cookie}completeHandshake(e,t){return new i(this.rawTimeout,e,this.cookie,t)}};var Ie=["84:d8:1b","78:8c:b5","cc:ba:bd","e4:fa:c4","ac:84:c6","50:c7:bf"],xe=5,re=i=>(i||"").replace(/:/g,"").toLowerCase(),Ce=i=>{let e=(i||"").toLowerCase();return Ie.some(t=>e.startsWith(t))},He=async i=>{let e=re(i),t=0;for(;t<=xe;){let r=(await se({skipNameResolution:!0})).find(o=>re(o.mac)===e);if(r?.ip)return r.ip;t++;}},Ke=async i=>{let e=await se({skipNameResolution:!0}),{email:t,password:s}=i;return e.filter(r=>Ce(r.mac)).map(r=>({ip:r.ip,mac:r.mac,loginDevice:async()=>{let o=new k({email:t,password:s}),a=await o.auth(r.ip,{email:t,password:s});if(!a.success.includes(r.ip)){let n=a.failed.find(c=>c.ip===r.ip);throw new Error(n?.error||`Failed to authenticate with device ${r.ip}`)}return o}}))},qe=async i=>{let e=new T(i),t=i.token;!t&&i.email&&i.password&&(t=await e.auth({email:i.email,password:i.password}));let s={method:"getDeviceList"},r=await C({method:"post",url:`${e._baseUrl}?token=${t}`,data:s});g(r.data);let o=[];for(let a of r.data.result?.deviceList||[]){if(!a.ip){let n=await He(a.deviceMac);if(!n)continue;a.ip=n;}switch(a.deviceType){case"IOT.SMARTBULB":case"SMART.TAPOBULB":{if(i.types&&!i.types.includes(ILumiaDeviceType.LIGHT))return;let n=a.deviceType==="SMART.TAPOBULB",c=(n?I(a.alias):a.alias)??a.deviceName;o.push({name:c,id:a.deviceMac,address:`http://${a.ip}`,host:a.ip,lumiaInfo:{alias:c,identifier:a.deviceMac,serial:a.hwId,lumiaType:ILumiaDeviceType.LIGHT,zonable:!1,maxZones:1,zones:[],rgb:!0,white:!0,connectionType:ILumiaDeviceConnectionType.WIFI,brand:n?ILumiaDeviceBrands.TPLINK:ILumiaDeviceBrands.TPLINK,product:a?.deviceModel}});break}case"IOT.SMARTPLUGSWITCH":case"SMART.TAPOPLUG":{if(i.types&&!i.types.includes(ILumiaDeviceType.PLUG))return;let n=a.deviceType==="SMART.TAPOPLUG",c=(n?I(a.alias):a.alias)??a.deviceName;o.push({name:c,id:a.deviceMac,address:`http://${a.ip}`,host:a.ip,lumiaInfo:{alias:c,identifier:a.deviceMac,serial:a.hwId,lumiaType:ILumiaDeviceType.PLUG,zonable:!1,maxZones:0,zones:[],rgb:!1,white:!1,connectionType:ILumiaDeviceConnectionType.WIFI,brand:n?ILumiaDeviceBrands.TPLINK:ILumiaDeviceBrands.TPLINK,product:a?.deviceModel}});break}}}return o},Ue=qe;var oe={};ne(oe,{DeviceResTypes:()=>Oe,DeviceSendValues:()=>Me,ETapoDeviceTypes:()=>ie,TapoDeviceTypes:()=>$e});var $e={ALL:"all",BULBS:"bulb",LIGHTSTRIPS:"lightstrip",PLUGS:"plug"},ie=(r=>(r.ALL="all",r.BULBS="bulb",r.LIGHTSTRIPS="lightstrip",r.PLUGS="plug",r))(ie||{}),Oe={BULB:"IOT.SMARTPLUGSWITCH",PLUG:"IOT.SMARTPLUGSWITCH"},Me={BULB:"smartlife.iot.smartbulb.lightingservice",LIGHTSTRIP:"smartlife.iot.lightStrip",PLUG:"system"};
|
|
14
14
|
|
|
15
|
-
export { w as LightState,
|
|
15
|
+
export { w as LightState, k as TapoApi, T as TapoCloudApi, oe as TapoConstants, Ue as discover, Ke as discoverLocalDevices };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumiastream/tapo-cove",
|
|
3
|
-
"version": "3.24.
|
|
3
|
+
"version": "3.24.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "GPL",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
"tsup": "*",
|
|
39
39
|
"typescript": "*"
|
|
40
40
|
},
|
|
41
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "ea9b70fe08856f069110965a9d027d18805baa6e"
|
|
42
42
|
}
|