Compare commits

...

3 Commits

39 changed files with 9 additions and 202 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
modelPics/C200_big.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
modelPics/H100_big.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
modelPics/P100_big.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
modelPics/P115_big.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
modelPics/S3SN-0U12A.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
modelPics/T110_big.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
modelPics_backup/C200.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
modelPics_backup/H100.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
modelPics_backup/P100.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

BIN
modelPics_backup/P115.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
modelPics_backup/T110.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -631,6 +631,10 @@ const dashboardHTML = `<!DOCTYPE html>
if (!devices[mac]) { if (!devices[mac]) {
// New device, request full refresh // New device, request full refresh
console.log('New device detected (' + mac + '), requesting refresh...');
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'get_status' }));
}
return; return;
} }
@@ -961,6 +965,11 @@ wss.on('connection', async (ws) => {
} }
} }
ws.send(JSON.stringify({ type: 'set_config_result', success })); ws.send(JSON.stringify({ type: 'set_config_result', success }));
} else if (data.type === 'get_status') {
// Client requested full status refresh
const devices = await getStatusData();
const rules = await getRulesStatus();
ws.send(JSON.stringify({ type: 'init', devices, rules }));
} }
} catch (err) { } catch (err) {
console.error('[Status] Error processing message:', err); console.error('[Status] Error processing message:', err);

View File

@@ -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);

View File

@@ -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();
});
}