feat: Kasa smart plug integration

This commit is contained in:
fullsizemalt 2026-01-05 23:19:13 -08:00
parent bbaffd4eff
commit 74f208c0c3
3 changed files with 114 additions and 2 deletions

View file

@ -22,14 +22,18 @@ export class HeartbeatSender {
private startTime: Date;
private getData: () => Omit<HeartbeatData, 'uptime'>;
private onCommand?: (commands: any[]) => void;
constructor(
backendUrl: string,
apiKey: string,
getData: () => Omit<HeartbeatData, 'uptime'>
getData: () => Omit<HeartbeatData, 'uptime'>,
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);

View file

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

81
src/kasa.ts Normal file
View file

@ -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<string | null> {
// 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<boolean> {
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;
}
}
}