feat: Kasa REST API and status endpoint
This commit is contained in:
parent
5ef80be739
commit
3847d2cf26
3 changed files with 133 additions and 7 deletions
|
|
@ -1,9 +1,11 @@
|
|||
/**
|
||||
* Health Check HTTP Server
|
||||
*
|
||||
* Exposes /health and /metrics endpoints for monitoring.
|
||||
* Exposes /health, /metrics, and /kasa endpoints for monitoring and control.
|
||||
*/
|
||||
|
||||
import { KasaController } from './kasa';
|
||||
|
||||
interface HealthStatus {
|
||||
status: 'ok' | 'error';
|
||||
lastSync: string | null;
|
||||
|
|
@ -12,17 +14,36 @@ interface HealthStatus {
|
|||
|
||||
type HealthCallback = () => HealthStatus;
|
||||
|
||||
// Shared Kasa controller instance
|
||||
let kasaController: KasaController | null = null;
|
||||
|
||||
export function setKasaController(controller: KasaController): void {
|
||||
kasaController = controller;
|
||||
}
|
||||
|
||||
export function startHealthServer(port: number, getHealth: HealthCallback): void {
|
||||
Bun.serve({
|
||||
port,
|
||||
hostname: '127.0.0.1', // Only bind to localhost for security
|
||||
fetch(req) {
|
||||
hostname: '0.0.0.0', // Bind to all interfaces for admin access
|
||||
async fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
|
||||
// CORS headers for frontend access
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
};
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
if (url.pathname === '/health') {
|
||||
const health = getHealth();
|
||||
return Response.json(health, {
|
||||
status: health.status === 'ok' ? 200 : 503,
|
||||
headers: corsHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -39,11 +60,69 @@ export function startHealthServer(port: number, getHealth: HealthCallback): void
|
|||
].join('\n');
|
||||
|
||||
return new Response(metrics, {
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
headers: { 'Content-Type': 'text/plain', ...corsHeaders },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response('Not Found', { status: 404 });
|
||||
// Kasa Status
|
||||
if (url.pathname === '/kasa/status') {
|
||||
if (!kasaController) {
|
||||
return Response.json({ error: 'Kasa controller not initialized' }, {
|
||||
status: 503,
|
||||
headers: corsHeaders
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const status = await kasaController.getStatus();
|
||||
return Response.json(status, { headers: corsHeaders });
|
||||
} catch (error: any) {
|
||||
return Response.json({ error: error.message, isOn: null }, { headers: corsHeaders });
|
||||
}
|
||||
}
|
||||
|
||||
// Kasa Control (POST)
|
||||
if (url.pathname === '/kasa/power' && req.method === 'POST') {
|
||||
if (!kasaController) {
|
||||
return Response.json({ error: 'Kasa controller not initialized' }, {
|
||||
status: 503,
|
||||
headers: corsHeaders
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await req.json() as { state: boolean };
|
||||
const success = await kasaController.setState(body.state);
|
||||
return Response.json({ success, state: body.state }, { headers: corsHeaders });
|
||||
} catch (error: any) {
|
||||
return Response.json({ error: error.message, success: false }, {
|
||||
status: 500,
|
||||
headers: corsHeaders
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Kasa Discover
|
||||
if (url.pathname === '/kasa/discover' && req.method === 'POST') {
|
||||
if (!kasaController) {
|
||||
return Response.json({ error: 'Kasa controller not initialized' }, {
|
||||
status: 503,
|
||||
headers: corsHeaders
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const ip = await kasaController.discover();
|
||||
return Response.json({ found: !!ip, ip }, { headers: corsHeaders });
|
||||
} catch (error: any) {
|
||||
return Response.json({ error: error.message, found: false }, {
|
||||
status: 500,
|
||||
headers: corsHeaders
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new Response('Not Found', { status: 404, headers: corsHeaders });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
import { SensorPushClient } from './sensorpush';
|
||||
import { VeridianClient } from './veridian';
|
||||
import { BufferManager } from './buffer';
|
||||
import { startHealthServer } from './health';
|
||||
import { startHealthServer, setKasaController } from './health';
|
||||
import { loadConfig, type EdgeConfig } from './config';
|
||||
import { AlertEngine, type Alert } from './alerts';
|
||||
import { HeartbeatSender } from './heartbeat';
|
||||
|
|
@ -81,7 +81,8 @@ async function main() {
|
|||
await sensorPush.authenticate();
|
||||
console.log('✅ SensorPush authenticated');
|
||||
|
||||
// Start health server
|
||||
// Start health server with Kasa controller access
|
||||
setKasaController(kasa);
|
||||
startHealthServer(3030, () => ({
|
||||
status: 'ok',
|
||||
lastSync: lastSync?.toISOString() ?? null,
|
||||
|
|
|
|||
46
src/kasa.ts
46
src/kasa.ts
|
|
@ -78,4 +78,50 @@ export class KasaController {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current status of the plug
|
||||
*/
|
||||
async getStatus(): Promise<{
|
||||
isOn: boolean | null;
|
||||
alias: string;
|
||||
ip: string | null;
|
||||
model: string;
|
||||
connected: boolean;
|
||||
}> {
|
||||
if (!this.deviceIp) {
|
||||
await this.discover();
|
||||
}
|
||||
|
||||
if (!this.deviceIp) {
|
||||
return {
|
||||
isOn: null,
|
||||
alias: 'Unknown',
|
||||
ip: null,
|
||||
model: 'Unknown',
|
||||
connected: false
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const device = await this.client.getDevice({ host: this.deviceIp });
|
||||
const sysInfo = await device.getSysInfo();
|
||||
return {
|
||||
isOn: sysInfo.relay_state === 1,
|
||||
alias: sysInfo.alias || 'Kasa Plug',
|
||||
ip: this.deviceIp,
|
||||
model: sysInfo.model || 'Unknown',
|
||||
connected: true
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('🔌 Kasa: Failed to get status', e);
|
||||
return {
|
||||
isOn: null,
|
||||
alias: 'Error',
|
||||
ip: this.deviceIp,
|
||||
model: 'Unknown',
|
||||
connected: false
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue