diff --git a/src/heartbeat.ts b/src/heartbeat.ts index c1a379b..49b3123 100644 --- a/src/heartbeat.ts +++ b/src/heartbeat.ts @@ -22,14 +22,18 @@ export class HeartbeatSender { private startTime: Date; private getData: () => Omit; + private onCommand?: (commands: any[]) => void; + constructor( backendUrl: string, apiKey: string, - getData: () => Omit + getData: () => Omit, + onCommand?: (commands: any[]) => void ) { this.backendUrl = backendUrl.replace(/\/$/, ''); this.apiKey = apiKey; this.getData = getData; + this.onCommand = onCommand; this.startTime = new Date(); } @@ -87,6 +91,15 @@ export class HeartbeatSender { return false; } + // Process instructions from server + const responseData = await res.json() as any; + if (responseData.commands && Array.isArray(responseData.commands) && responseData.commands.length > 0) { + console.log(`📥 Received ${responseData.commands.length} commands from server`); + if (this.onCommand) { + this.onCommand(responseData.commands); + } + } + return true; } catch (error) { console.warn('⚠️ Heartbeat error:', error); diff --git a/src/index.ts b/src/index.ts index 9488cfb..6d5d16e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ import { startHealthServer } from './health'; import { loadConfig, type EdgeConfig } from './config'; import { AlertEngine, type Alert } from './alerts'; import { HeartbeatSender } from './heartbeat'; +import { KasaController } from './kasa'; // Global state let config: EdgeConfig; @@ -23,6 +24,7 @@ let veridian: VeridianClient; let buffer: BufferManager; let alertEngine: AlertEngine; let heartbeat: HeartbeatSender; +let kasa: KasaController; let lastSync: Date | null = null; let lastReading: Date | null = null; let isRunning = false; @@ -41,6 +43,7 @@ async function main() { sensorPush = new SensorPushClient(config.sensorpush); veridian = new VeridianClient(config.server.url, config.server.apiKey); buffer = new BufferManager(config.storage.maxRows); + kasa = new KasaController(); // Initialize alert engine alertEngine = new AlertEngine(config.alerts, handleAlert); @@ -56,7 +59,22 @@ async function main() { sensorCount, bufferSize: buffer.count(), lastReading: lastReading?.toISOString(), - }) + }), + // Handle commands from backend + async (commands: any[]) => { + for (const cmd of commands) { + console.log(`🤖 Processing command: ${cmd.type}`, cmd); + + if (cmd.type === 'toggle_plug') { + const success = await kasa.setState(cmd.state); + if (success) { + console.log(`✅ Kasa plug toggled to ${cmd.state}`); + } else { + console.error('❌ Failed to toggle Kasa plug'); + } + } + } + } ); // Authenticate with SensorPush diff --git a/src/kasa.ts b/src/kasa.ts new file mode 100644 index 0000000..0abe888 --- /dev/null +++ b/src/kasa.ts @@ -0,0 +1,81 @@ + +import { Client } from 'tplink-smarthome-api'; + +export class KasaController { + private client: Client; + private deviceIp: string | null = null; + private lastDiscovery: number = 0; + + constructor() { + this.client = new Client(); + } + + /** + * Discover the Kasa device on local network. + * Prioritizes EP10 model as requested. + */ + async discover(): Promise { + // Prevent spamming discovery + if (Date.now() - this.lastDiscovery < 10000 && !this.deviceIp) { + return null; + } + this.lastDiscovery = Date.now(); + + return new Promise((resolve) => { + console.log('🔌 Kasa: Starting discovery...'); + let found = false; + + this.client.startDiscovery({ + discoveryInterval: 500, + discoveryTimeout: 2000 + }).on('device-new', (device) => { + const model = device.model; + console.log(`🔌 Kasa: Found device ${device.alias} (${model}) at ${device.host}`); + + // If we found an EP10, grab it immediately + if (model.toLowerCase().includes('ep10')) { + this.deviceIp = device.host; + found = true; + this.client.stopDiscovery(); + resolve(this.deviceIp); + } + }); + + // Timeout after 3s + setTimeout(() => { + this.client.stopDiscovery(); + if (!found) { + if (this.deviceIp) console.log(`🔌 Kasa: Using previously known IP: ${this.deviceIp}`); + else console.warn('🔌 Kasa: Discovery timed out, no device found'); + resolve(this.deviceIp); + } + }, 3000); + }); + } + + /** + * Toggle the plug power state + */ + async setState(state: boolean): Promise { + if (!this.deviceIp) { + await this.discover(); + } + + if (!this.deviceIp) { + console.error('🔌 Kasa: No device found to control'); + return false; + } + + try { + const device = await this.client.getDevice({ host: this.deviceIp }); + await device.setPowerState(state); + console.log(`🔌 Kasa: Set power to ${state ? 'ON' : 'OFF'}`); + return true; + } catch (e) { + console.error('🔌 Kasa: Failed to set state', e); + // Invalidate IP in case it changed + this.deviceIp = null; + return false; + } + } +}