feat: Add Tapo device integration with discovery and client, and generalize the status server to display IoT status.
This commit is contained in:
63
server.js
63
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(() => {
|
||||
|
||||
Reference in New Issue
Block a user