diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..eabb05c --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Tapo Credentials +# Your TP-Link/Tapo account credentials +TAPO_USERNAME=your-email@example.com +TAPO_PASSWORD=your-password + +# Tapo Discovery Settings (optional) +TAPO_BROADCAST_ADDR=192.168.3.255 +TAPO_DISCOVERY_INTERVAL=300000 +TAPO_POLL_INTERVAL=10000 diff --git a/.gitignore b/.gitignore index 8dc6917..030f340 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ node_modules/ logs/ devices.db -rules/timer_state.json \ No newline at end of file +rules/timer_state.json +.env +tapo/ \ No newline at end of file diff --git a/modelPics/C200.png b/modelPics/C200.png new file mode 100644 index 0000000..4d5d6ac Binary files /dev/null and b/modelPics/C200.png differ diff --git a/modelPics/H100(EU).png b/modelPics/H100(EU).png new file mode 100644 index 0000000..54a2967 Binary files /dev/null and b/modelPics/H100(EU).png differ diff --git a/modelPics/P100.png b/modelPics/P100.png new file mode 100644 index 0000000..4c3bc32 Binary files /dev/null and b/modelPics/P100.png differ diff --git a/modelPics/P115(EU).png b/modelPics/P115(EU).png new file mode 100644 index 0000000..e11d30f Binary files /dev/null and b/modelPics/P115(EU).png differ diff --git a/modelPics/T110.png b/modelPics/T110.png new file mode 100644 index 0000000..aa64eea Binary files /dev/null and b/modelPics/T110.png differ diff --git a/package-lock.json b/package-lock.json index 7fde225..f16442e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "shellyagent", "version": "1.0.0", "dependencies": { + "dotenv": "^17.2.3", "sqlite3": "^5.1.7", "ws": "^8.19.0" } @@ -347,6 +348,18 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", diff --git a/package.json b/package.json index e6b9bfa..595cd41 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "start": "node server.js" }, "dependencies": { + "dotenv": "^17.2.3", "sqlite3": "^5.1.7", "ws": "^8.19.0" } diff --git a/server.js b/server.js index c6f46e2..d30a1aa 100644 --- a/server.js +++ b/server.js @@ -1,3 +1,4 @@ +import 'dotenv/config'; import { WebSocketServer } from 'ws'; import fs from 'fs'; import path from 'path'; @@ -5,6 +6,7 @@ import sqlite3 from 'sqlite3'; import { fileURLToPath } from 'url'; import { initRuleEngine, loadRules, runRules, watchRules } from './rule_engine.js'; import { broadcastEvent, broadcastRuleUpdate, startStatusServer } from './status_server.js'; +import { TapoManager } from './tapo_client.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -185,6 +187,61 @@ loadRules().then(() => { // Start status dashboard server startStatusServer(); +// Initialize Tapo Manager if credentials are configured +let tapoManager = null; +if (process.env.TAPO_USERNAME && process.env.TAPO_PASSWORD) { + console.log('[Tapo] Credentials found, initializing Tapo manager...'); + + tapoManager = new TapoManager(process.env.TAPO_USERNAME, process.env.TAPO_PASSWORD, { + broadcastAddr: process.env.TAPO_BROADCAST_ADDR || '255.255.255.255', + discoveryInterval: parseInt(process.env.TAPO_DISCOVERY_INTERVAL) || 5 * 60 * 1000, // 5 minutes + pollInterval: parseInt(process.env.TAPO_POLL_INTERVAL) || 10 * 1000, // 10 seconds + discoveryTimeout: 10 + }); + + // Handle Tapo device discovery (initial UDP discovery - before we have real MAC) + tapoManager.onDeviceDiscovered = (deviceInfo) => { + console.log(`[Tapo] Device discovered: ${deviceInfo.deviceModel} at ${deviceInfo.ip}`); + // Note: Real MAC and nickname will be updated when we first poll the device + }; + + // Handle Tapo child device discovery (H100 hub sensors) + tapoManager.onChildDeviceDiscovered = (hubInfo, childInfo) => { + console.log(`[Tapo] Child device discovered: ${childInfo.nickname || childInfo.device_id} (${childInfo.model}) on hub`); + // Note: MAC and nickname will be updated when we poll + }; + + // Handle Tapo state changes - route through the same event system as Shelly + // Now receives: (mac, component, field, type, value, deviceInfo) + tapoManager.onDeviceStateChange = (mac, component, field, type, value, deviceInfo) => { + // Update device record if we have device info with nickname + if (deviceInfo && deviceInfo.mac) { + const model = deviceInfo.model || 'Unknown'; + const nickname = deviceInfo.nickname + ? Buffer.from(deviceInfo.nickname, 'base64').toString('utf8').replace(/\0/g, '') + : null; + + const stmt = db.prepare("INSERT OR REPLACE INTO devices (mac, model, connected, last_seen) VALUES (?, ?, ?, ?)"); + stmt.run(mac, model, value === true && component === 'system' && field === 'online' ? 1 : null, new Date().toISOString()); + stmt.finalize(); + + if (nickname) { + console.log(`[Tapo] ${nickname} (${model}) ${mac}: ${component}.${field} = ${value}`); + } + } + + // Log the event through the standard event system + checkAndLogEvent(mac, component, field, type, value, null); + }; + + // Start the Tapo manager + tapoManager.start().catch(err => { + console.error('[Tapo] Failed to start manager:', err); + }); +} else { + console.log('[Tapo] No credentials configured. Set TAPO_USERNAME and TAPO_PASSWORD in .env to enable Tapo integration.'); +} + // Global counter for connection IDs let connectionIdCounter = 0; @@ -441,6 +498,12 @@ wss.on('connection', (ws, req) => { // Graceful shutdown function shutdown() { console.log('Shutting down server...'); + + // Stop Tapo manager + if (tapoManager) { + tapoManager.stop(); + } + wss.clients.forEach(ws => ws.terminate()); db.serialize(() => { diff --git a/status_server.js b/status_server.js index 9ac23ad..ee43d82 100644 --- a/status_server.js +++ b/status_server.js @@ -101,7 +101,7 @@ const dashboardHTML = ` - Shelly Status + IoT Status