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
|
* 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 {
|
interface HealthStatus {
|
||||||
status: 'ok' | 'error';
|
status: 'ok' | 'error';
|
||||||
lastSync: string | null;
|
lastSync: string | null;
|
||||||
|
|
@ -12,17 +14,36 @@ interface HealthStatus {
|
||||||
|
|
||||||
type HealthCallback = () => 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 {
|
export function startHealthServer(port: number, getHealth: HealthCallback): void {
|
||||||
Bun.serve({
|
Bun.serve({
|
||||||
port,
|
port,
|
||||||
hostname: '127.0.0.1', // Only bind to localhost for security
|
hostname: '0.0.0.0', // Bind to all interfaces for admin access
|
||||||
fetch(req) {
|
async fetch(req) {
|
||||||
const url = new URL(req.url);
|
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') {
|
if (url.pathname === '/health') {
|
||||||
const health = getHealth();
|
const health = getHealth();
|
||||||
return Response.json(health, {
|
return Response.json(health, {
|
||||||
status: health.status === 'ok' ? 200 : 503,
|
status: health.status === 'ok' ? 200 : 503,
|
||||||
|
headers: corsHeaders,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,11 +60,69 @@ export function startHealthServer(port: number, getHealth: HealthCallback): void
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
return new Response(metrics, {
|
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 { SensorPushClient } from './sensorpush';
|
||||||
import { VeridianClient } from './veridian';
|
import { VeridianClient } from './veridian';
|
||||||
import { BufferManager } from './buffer';
|
import { BufferManager } from './buffer';
|
||||||
import { startHealthServer } from './health';
|
import { startHealthServer, setKasaController } 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';
|
||||||
|
|
@ -81,7 +81,8 @@ async function main() {
|
||||||
await sensorPush.authenticate();
|
await sensorPush.authenticate();
|
||||||
console.log('✅ SensorPush authenticated');
|
console.log('✅ SensorPush authenticated');
|
||||||
|
|
||||||
// Start health server
|
// Start health server with Kasa controller access
|
||||||
|
setKasaController(kasa);
|
||||||
startHealthServer(3030, () => ({
|
startHealthServer(3030, () => ({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
lastSync: lastSync?.toISOString() ?? null,
|
lastSync: lastSync?.toISOString() ?? null,
|
||||||
|
|
|
||||||
46
src/kasa.ts
46
src/kasa.ts
|
|
@ -78,4 +78,50 @@ export class KasaController {
|
||||||
return false;
|
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