feat: Add Tapo device integration with discovery and client, and generalize the status server to display IoT status.

This commit is contained in:
sebseb7
2026-01-21 18:13:36 -05:00
parent b6a25a53fc
commit e619acd0da
13 changed files with 1203 additions and 3 deletions

View File

@@ -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(() => {