diff --git a/modelPics/S3SN-0U12A.png b/modelPics/S3SN-0U12A.png new file mode 100644 index 0000000..ad4367f Binary files /dev/null and b/modelPics/S3SN-0U12A.png differ diff --git a/modelPics/S3SN-0U12A_big.png b/modelPics/S3SN-0U12A_big.png new file mode 100644 index 0000000..35c0f82 Binary files /dev/null and b/modelPics/S3SN-0U12A_big.png differ diff --git a/modelPics_backup/S3SN-0U12A.png b/modelPics_backup/S3SN-0U12A.png new file mode 100644 index 0000000..9ad6e76 Binary files /dev/null and b/modelPics_backup/S3SN-0U12A.png differ diff --git a/tapo_test.js b/tapo_test.js deleted file mode 100644 index afcfb70..0000000 --- a/tapo_test.js +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Tapo Device Info Test - * - * This script polls each Tapo device type to see what data is available. - */ - -import 'dotenv/config'; -import crypto from 'crypto'; -import http from 'http'; - -// KLAP Cipher -class KlapCipher { - constructor(localSeed, remoteSeed, authHash) { - const localHash = Buffer.concat([localSeed, remoteSeed, authHash]); - this.key = this._keyDerive(localHash); - const { iv, seq } = this._ivDerive(localHash); - this.iv = iv; - this.seq = seq; - this.sig = this._sigDerive(localHash); - } - - static sha1(data) { return crypto.createHash('sha1').update(data).digest(); } - static sha256(data) { return crypto.createHash('sha256').update(data).digest(); } - - _keyDerive(h) { return KlapCipher.sha256(Buffer.concat([Buffer.from('lsk'), h])).subarray(0, 16); } - _ivDerive(h) { - const hash = KlapCipher.sha256(Buffer.concat([Buffer.from('iv'), h])); - return { iv: hash.subarray(0, 12), seq: hash.readInt32BE(hash.length - 4) }; - } - _sigDerive(h) { return KlapCipher.sha256(Buffer.concat([Buffer.from('ldk'), h])).subarray(0, 28); } - _ivSeq(seq) { const b = Buffer.alloc(4); b.writeInt32BE(seq); return Buffer.concat([this.iv, b]); } - - encrypt(data) { - this.seq++; - const cipher = crypto.createCipheriv('aes-128-cbc', this.key, this._ivSeq(this.seq)); - const encrypted = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]); - const sigData = Buffer.concat([this.sig, Buffer.from([(this.seq >> 24) & 0xff, (this.seq >> 16) & 0xff, (this.seq >> 8) & 0xff, this.seq & 0xff]), encrypted]); - return { payload: Buffer.concat([KlapCipher.sha256(sigData), encrypted]), seq: this.seq }; - } - - decrypt(seq, data) { - const decipher = crypto.createDecipheriv('aes-128-cbc', this.key, this._ivSeq(seq)); - return Buffer.concat([decipher.update(data.subarray(32)), decipher.final()]).toString('utf8'); - } -} - -function httpRequest(ip, path, body, cookie = null) { - return new Promise((resolve, reject) => { - const options = { - hostname: ip, port: 80, path: `/app${path}`, method: 'POST', - headers: { 'Content-Type': 'application/octet-stream', 'Content-Length': body.length }, - timeout: 5000 - }; - if (cookie) options.headers['Cookie'] = cookie; - - const req = http.request(options, (res) => { - const chunks = []; - res.on('data', chunk => chunks.push(chunk)); - res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks) })); - }); - req.on('error', reject); - req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); }); - req.write(body); - req.end(); - }); -} - -async function connectDevice(ip, username, password) { - const authHash = KlapCipher.sha256(Buffer.concat([ - KlapCipher.sha1(Buffer.from(username.toLowerCase().trim())), - KlapCipher.sha1(Buffer.from(password)) - ])); - - const localSeed = crypto.randomBytes(16); - const hs1 = await httpRequest(ip, '/handshake1', localSeed); - if (hs1.status !== 200) throw new Error(`Handshake1 failed: ${hs1.status}`); - - const cookies = hs1.headers['set-cookie']; - const cookieStr = Array.isArray(cookies) ? cookies[0] : cookies; - const cookie = cookieStr.match(/TP_SESSIONID=([^;]+)/)?.[0] || ''; - - const remoteSeed = hs1.body.subarray(0, 16); - const serverHash = hs1.body.subarray(16, 48); - const localHash = KlapCipher.sha256(Buffer.concat([localSeed, remoteSeed, authHash])); - if (!localHash.equals(serverHash)) throw new Error('Auth failed'); - - const hs2Payload = KlapCipher.sha256(Buffer.concat([remoteSeed, localSeed, authHash])); - const hs2 = await httpRequest(ip, '/handshake2', hs2Payload, cookie); - if (hs2.status !== 200) throw new Error(`Handshake2 failed: ${hs2.status}`); - - return { cipher: new KlapCipher(localSeed, remoteSeed, authHash), cookie }; -} - -async function request(ip, cipher, cookie, method, params = {}) { - const requestData = JSON.stringify({ method, params }); - const { payload, seq } = cipher.encrypt(requestData); - const response = await httpRequest(ip, `/request?seq=${seq}`, payload, cookie); - if (response.status !== 200) throw new Error(`Request failed: ${response.status}`); - return JSON.parse(cipher.decrypt(seq, response.body)); -} - -async function testDevice(ip, type) { - console.log(`\n${'='.repeat(60)}`); - console.log(`Testing ${type} at ${ip}`); - console.log('='.repeat(60)); - - try { - const { cipher, cookie } = await connectDevice(ip, process.env.TAPO_USERNAME, process.env.TAPO_PASSWORD); - - // Get device info - const info = await request(ip, cipher, cookie, 'get_device_info'); - console.log('\nšŸ“± get_device_info result:'); - console.log(JSON.stringify(info.result, null, 2)); - - // For hubs, get child devices - if (type.includes('H100')) { - const children = await request(ip, cipher, cookie, 'get_child_device_list'); - console.log('\nšŸ‘¶ get_child_device_list result:'); - console.log(JSON.stringify(children.result, null, 2)); - } - - } catch (e) { - console.log(`āŒ Error: ${e.message}`); - } -} - -async function main() { - console.log('Tapo Device Info Test'); - console.log('Username:', process.env.TAPO_USERNAME); - - // Test each device type - await testDevice('192.168.3.18', 'P100'); - await testDevice('192.168.3.21', 'P115(EU)'); - await testDevice('192.168.3.28', 'H100(EU)'); - await testDevice('192.168.3.45', 'C200'); - await testDevice('192.168.3.22', 'C200 #2'); -} - -main().catch(console.error); diff --git a/test_db_upsert.js b/test_db_upsert.js deleted file mode 100644 index 48965e0..0000000 --- a/test_db_upsert.js +++ /dev/null @@ -1,63 +0,0 @@ -import sqlite3 from 'sqlite3'; - -const db = new sqlite3.Database(':memory:'); - -db.serialize(() => { - db.run("CREATE TABLE devices (mac TEXT PRIMARY KEY, model TEXT, connected INTEGER, last_seen TEXT)"); -}); - -const upsertSql = ` - INSERT INTO devices (mac, model, connected, last_seen) - VALUES (?, ?, ?, ?) - ON CONFLICT(mac) DO UPDATE SET - model = excluded.model, - connected = COALESCE(excluded.connected, devices.connected), - last_seen = excluded.last_seen -`; - -console.log("Starting sequential tests..."); - -// Test 1: Insert initial online device -db.run(upsertSql, ['AA:BB:CC', 'T110', 1, '2023-01-01T12:00:00Z'], (err) => { - if (err) console.error(err); - checkDevice('Test 1 (Init Online)', 'AA:BB:CC', 1, () => { - - // Test 2: Update with NULL status - db.run(upsertSql, ['AA:BB:CC', 'T110', null, '2023-01-01T12:05:00Z'], (err) => { - if (err) console.error(err); - checkDevice('Test 2 (Sensor Event/Null)', 'AA:BB:CC', 1, () => { - - // Test 3: Update with offline status - db.run(upsertSql, ['AA:BB:CC', 'T110', 0, '2023-01-01T12:10:00Z'], (err) => { - if (err) console.error(err); - checkDevice('Test 3 (Offline/0)', 'AA:BB:CC', 0, () => { - - // Test 4: Update with NULL status while offline - db.run(upsertSql, ['AA:BB:CC', 'T110', null, '2023-01-01T12:15:00Z'], (err) => { - if (err) console.error(err); - checkDevice('Test 4 (Sensor Event while Offline)', 'AA:BB:CC', 0, () => { - - // Test 5: Back online - db.run(upsertSql, ['AA:BB:CC', 'T110', 1, '2023-01-01T12:20:00Z'], (err) => { - if (err) console.error(err); - checkDevice('Test 5 (Online again)', 'AA:BB:CC', 1, () => { - console.log("All tests completed."); - }); - }); - }); - }); - }); - }); - }); - }); - }); -}); - -function checkDevice(testName, mac, expectedConnected, callback) { - db.get("SELECT * FROM devices WHERE mac = ?", [mac], (err, row) => { - if (err) console.error("DB Error:", err); - const status = row.connected === expectedConnected ? 'PASS' : 'FAIL'; - console.log(`${testName}: connected=${row.connected} (Expected: ${expectedConnected}) -> ${status}`); - if (callback) callback(); - }); -}