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 startTime: Date;
|
||||||
private getData: () => Omit<HeartbeatData, 'uptime'>;
|
private getData: () => Omit<HeartbeatData, 'uptime'>;
|
||||||
|
|
||||||
|
private onCommand?: (commands: any[]) => void;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
backendUrl: string,
|
backendUrl: string,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
getData: () => Omit<HeartbeatData, 'uptime'>
|
getData: () => Omit<HeartbeatData, 'uptime'>,
|
||||||
|
onCommand?: (commands: any[]) => void
|
||||||
) {
|
) {
|
||||||
this.backendUrl = backendUrl.replace(/\/$/, '');
|
this.backendUrl = backendUrl.replace(/\/$/, '');
|
||||||
this.apiKey = apiKey;
|
this.apiKey = apiKey;
|
||||||
this.getData = getData;
|
this.getData = getData;
|
||||||
|
this.onCommand = onCommand;
|
||||||
this.startTime = new Date();
|
this.startTime = new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,6 +91,15 @@ export class HeartbeatSender {
|
||||||
return false;
|
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;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('⚠️ Heartbeat error:', 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 { loadConfig, type EdgeConfig } from './config';
|
||||||
import { AlertEngine, type Alert } from './alerts';
|
import { AlertEngine, type Alert } from './alerts';
|
||||||
import { HeartbeatSender } from './heartbeat';
|
import { HeartbeatSender } from './heartbeat';
|
||||||
|
import { KasaController } from './kasa';
|
||||||
|
|
||||||
// Global state
|
// Global state
|
||||||
let config: EdgeConfig;
|
let config: EdgeConfig;
|
||||||
|
|
@ -23,6 +24,7 @@ let veridian: VeridianClient;
|
||||||
let buffer: BufferManager;
|
let buffer: BufferManager;
|
||||||
let alertEngine: AlertEngine;
|
let alertEngine: AlertEngine;
|
||||||
let heartbeat: HeartbeatSender;
|
let heartbeat: HeartbeatSender;
|
||||||
|
let kasa: KasaController;
|
||||||
let lastSync: Date | null = null;
|
let lastSync: Date | null = null;
|
||||||
let lastReading: Date | null = null;
|
let lastReading: Date | null = null;
|
||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
|
|
@ -41,6 +43,7 @@ async function main() {
|
||||||
sensorPush = new SensorPushClient(config.sensorpush);
|
sensorPush = new SensorPushClient(config.sensorpush);
|
||||||
veridian = new VeridianClient(config.server.url, config.server.apiKey);
|
veridian = new VeridianClient(config.server.url, config.server.apiKey);
|
||||||
buffer = new BufferManager(config.storage.maxRows);
|
buffer = new BufferManager(config.storage.maxRows);
|
||||||
|
kasa = new KasaController();
|
||||||
|
|
||||||
// Initialize alert engine
|
// Initialize alert engine
|
||||||
alertEngine = new AlertEngine(config.alerts, handleAlert);
|
alertEngine = new AlertEngine(config.alerts, handleAlert);
|
||||||
|
|
@ -56,7 +59,22 @@ async function main() {
|
||||||
sensorCount,
|
sensorCount,
|
||||||
bufferSize: buffer.count(),
|
bufferSize: buffer.count(),
|
||||||
lastReading: lastReading?.toISOString(),
|
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
|
// 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