feat: Kasa smart plug integration
This commit is contained in:
parent
bbaffd4eff
commit
74f208c0c3
3 changed files with 114 additions and 2 deletions
|
|
@ -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);
|
||||
|
|
|
|||
20
src/index.ts
20
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
|
||||
|
|
|
|||
81
src/kasa.ts
Normal file
81
src/kasa.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue