315 lines
12 KiB
TypeScript
315 lines
12 KiB
TypeScript
import { Router, Request, Response } from 'express';
|
|
import { z } from 'zod';
|
|
import { deviceService } from '../services/deviceService';
|
|
import { discoveryService } from '../services/discoveryService';
|
|
|
|
const router = Router();
|
|
|
|
const createDeviceSchema = z.object({
|
|
name: z.string().min(1, 'Name is required'),
|
|
ipAddress: z.string().min(1, 'IP address is required'),
|
|
port: z.number().int().positive().optional(),
|
|
enabled: z.boolean().optional(),
|
|
});
|
|
|
|
const updateDeviceSchema = z.object({
|
|
name: z.string().min(1).optional(),
|
|
ipAddress: z.string().min(1).optional(),
|
|
port: z.number().int().positive().optional(),
|
|
enabled: z.boolean().optional(),
|
|
});
|
|
|
|
// GET /api/devices
|
|
router.get('/', async (req: Request, res: Response) => {
|
|
try {
|
|
const devices = await deviceService.getAllDevices();
|
|
res.json(devices);
|
|
} catch (error) {
|
|
console.error('Error fetching devices:', error);
|
|
res.status(500).json({ error: 'InternalError', message: 'Failed to fetch devices' });
|
|
}
|
|
});
|
|
|
|
// GET /api/devices/:id
|
|
router.get('/:id', async (req: Request, res: Response) => {
|
|
try {
|
|
const device = await deviceService.getDeviceById(req.params.id);
|
|
if (!device) {
|
|
return res.status(404).json({ error: 'NotFound', message: 'Device not found' });
|
|
}
|
|
res.json(device);
|
|
} catch (error) {
|
|
console.error('Error fetching device:', error);
|
|
res.status(500).json({ error: 'InternalError', message: 'Failed to fetch device' });
|
|
}
|
|
});
|
|
|
|
// POST /api/devices
|
|
router.post('/', async (req: Request, res: Response) => {
|
|
try {
|
|
const body = createDeviceSchema.parse(req.body);
|
|
const device = await deviceService.createDevice(body);
|
|
res.status(201).json(device);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
return res.status(400).json({
|
|
error: 'ValidationError',
|
|
details: error.errors.map((e) => ({ field: e.path.join('.'), message: e.message })),
|
|
});
|
|
}
|
|
console.error('Error creating device:', error);
|
|
res.status(500).json({ error: 'InternalError', message: 'Failed to create device' });
|
|
}
|
|
});
|
|
|
|
// PUT /api/devices/:id
|
|
router.put('/:id', async (req: Request, res: Response) => {
|
|
try {
|
|
const body = updateDeviceSchema.parse(req.body);
|
|
const device = await deviceService.updateDevice(req.params.id, body);
|
|
res.json(device);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
return res.status(400).json({
|
|
error: 'ValidationError',
|
|
details: error.errors.map((e) => ({ field: e.path.join('.'), message: e.message })),
|
|
});
|
|
}
|
|
if (error instanceof Error && error.message.includes('Record to update not found')) {
|
|
return res.status(404).json({ error: 'NotFound', message: 'Device not found' });
|
|
}
|
|
console.error('Error updating device:', error);
|
|
res.status(500).json({ error: 'InternalError', message: 'Failed to update device' });
|
|
}
|
|
});
|
|
|
|
// DELETE /api/devices/:id
|
|
router.delete('/:id', async (req: Request, res: Response) => {
|
|
try {
|
|
await deviceService.deleteDevice(req.params.id);
|
|
res.status(204).send();
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message.includes('Record to delete does not exist')) {
|
|
return res.status(404).json({ error: 'NotFound', message: 'Device not found' });
|
|
}
|
|
console.error('Error deleting device:', error);
|
|
res.status(500).json({ error: 'InternalError', message: 'Failed to delete device' });
|
|
}
|
|
});
|
|
|
|
// POST /api/devices/:id/ping
|
|
router.post('/:id/ping', async (req: Request, res: Response) => {
|
|
try {
|
|
const result = await deviceService.pingDevice(req.params.id);
|
|
if (result.status === 'error') {
|
|
return res.status(502).json(result);
|
|
}
|
|
res.json(result);
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message === 'Device not found') {
|
|
return res.status(404).json({ error: 'NotFound', message: 'Device not found' });
|
|
}
|
|
console.error('Error pinging device:', error);
|
|
res.status(500).json({ error: 'InternalError', message: 'Failed to ping device' });
|
|
}
|
|
});
|
|
|
|
// GET /api/devices/:id/details
|
|
router.get('/:id/details', async (req: Request, res: Response) => {
|
|
try {
|
|
const details = await deviceService.getDeviceDetails(req.params.id);
|
|
res.json(details);
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message === 'Device not found') {
|
|
return res.status(404).json({ error: 'NotFound', message: 'Device not found' });
|
|
}
|
|
console.error('Error fetching device details:', error);
|
|
res.status(500).json({ error: 'InternalError', message: 'Failed to fetch device details' });
|
|
}
|
|
});
|
|
|
|
// GET /api/devices/:id/presets
|
|
router.get('/:id/presets', async (req: Request, res: Response) => {
|
|
try {
|
|
const presets = await deviceService.getDevicePresets(req.params.id);
|
|
res.json(presets);
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message === 'Device not found') {
|
|
return res.status(404).json({ error: 'NotFound', message: 'Device not found' });
|
|
}
|
|
console.error('Error fetching device presets:', error);
|
|
res.status(500).json({ error: 'InternalError', message: 'Failed to fetch presets' });
|
|
}
|
|
});
|
|
|
|
// GET /api/devices/status/all
|
|
router.get('/status/all', async (req: Request, res: Response) => {
|
|
try {
|
|
const devices = await deviceService.getAllDevicesWithStatus();
|
|
res.json(devices);
|
|
} catch (error) {
|
|
console.error('Error fetching devices with status:', error);
|
|
res.status(500).json({ error: 'InternalError', message: 'Failed to fetch devices status' });
|
|
}
|
|
});
|
|
|
|
// POST /api/devices/:id/turn-on
|
|
router.post('/:id/turn-on', async (req: Request, res: Response) => {
|
|
try {
|
|
await deviceService.turnOnDevice(req.params.id);
|
|
res.json({ status: 'ok', message: 'Device turned on' });
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message === 'Device not found') {
|
|
return res.status(404).json({ error: 'NotFound', message: 'Device not found' });
|
|
}
|
|
console.error('Error turning on device:', error);
|
|
res.status(500).json({ error: 'InternalError', message: 'Failed to turn on device' });
|
|
}
|
|
});
|
|
|
|
// POST /api/devices/:id/turn-off
|
|
router.post('/:id/turn-off', async (req: Request, res: Response) => {
|
|
try {
|
|
await deviceService.turnOffDevice(req.params.id);
|
|
res.json({ status: 'ok', message: 'Device turned off' });
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message === 'Device not found') {
|
|
return res.status(404).json({ error: 'NotFound', message: 'Device not found' });
|
|
}
|
|
console.error('Error turning off device:', error);
|
|
res.status(500).json({ error: 'InternalError', message: 'Failed to turn off device' });
|
|
}
|
|
});
|
|
|
|
// POST /api/devices/all/turn-on
|
|
router.post('/all/turn-on', async (req: Request, res: Response) => {
|
|
try {
|
|
const result = await deviceService.turnOnAllDevices();
|
|
res.json(result);
|
|
} catch (error) {
|
|
console.error('Error turning on all devices:', error);
|
|
res.status(500).json({ error: 'InternalError', message: 'Failed to turn on all devices' });
|
|
}
|
|
});
|
|
|
|
// POST /api/devices/all/turn-off
|
|
router.post('/all/turn-off', async (req: Request, res: Response) => {
|
|
try {
|
|
const result = await deviceService.turnOffAllDevices();
|
|
res.json(result);
|
|
} catch (error) {
|
|
console.error('Error turning off all devices:', error);
|
|
res.status(500).json({ error: 'InternalError', message: 'Failed to turn off all devices' });
|
|
}
|
|
});
|
|
|
|
// POST /api/devices/:id/sync
|
|
router.post('/:id/sync', async (req: Request, res: Response) => {
|
|
try {
|
|
const { targetIds } = z.object({
|
|
targetIds: z.array(z.string()).min(1, 'At least one target device is required'),
|
|
}).parse(req.body);
|
|
|
|
const result = await deviceService.syncDeviceState(req.params.id, targetIds);
|
|
res.json(result);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
return res.status(400).json({
|
|
error: 'ValidationError',
|
|
details: error.errors.map((e) => ({ field: e.path.join('.'), message: e.message })),
|
|
});
|
|
}
|
|
if (error instanceof Error && error.message.includes('not found')) {
|
|
return res.status(404).json({ error: 'NotFound', message: error.message });
|
|
}
|
|
console.error('Error syncing device state:', error);
|
|
res.status(500).json({ error: 'InternalError', message: 'Failed to sync device state' });
|
|
}
|
|
});
|
|
|
|
// POST /api/devices/:sourceId/copy-to/:targetId
|
|
router.post('/:sourceId/copy-to/:targetId', async (req: Request, res: Response) => {
|
|
try {
|
|
await deviceService.copyDeviceConfig(req.params.sourceId, req.params.targetId);
|
|
res.json({ status: 'ok', message: 'Configuration copied successfully' });
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message.includes('not found')) {
|
|
return res.status(404).json({ error: 'NotFound', message: error.message });
|
|
}
|
|
console.error('Error copying device config:', error);
|
|
res.status(500).json({ error: 'InternalError', message: 'Failed to copy configuration' });
|
|
}
|
|
});
|
|
|
|
// POST /api/devices/discover/mdns
|
|
router.post('/discover/mdns', async (req: Request, res: Response) => {
|
|
try {
|
|
const { timeout } = z.object({
|
|
timeout: z.number().int().positive().optional(),
|
|
}).parse(req.body);
|
|
|
|
const discoveredDevices = await discoveryService.discoverDevices(timeout);
|
|
res.json({ devices: discoveredDevices, count: discoveredDevices.length });
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
return res.status(400).json({
|
|
error: 'ValidationError',
|
|
details: error.errors.map((e) => ({ field: e.path.join('.'), message: e.message })),
|
|
});
|
|
}
|
|
console.error('Error discovering devices:', error);
|
|
res.status(500).json({ error: 'InternalError', message: 'Failed to discover devices' });
|
|
}
|
|
});
|
|
|
|
// POST /api/devices/discover/scan
|
|
router.post('/discover/scan', async (req: Request, res: Response) => {
|
|
try {
|
|
const { baseIp, startRange, endRange } = z.object({
|
|
baseIp: z.string().regex(/^\d{1,3}\.\d{1,3}\.\d{1,3}$/, 'Invalid base IP format (e.g., 192.168.1)'),
|
|
startRange: z.number().int().min(1).max(254).optional(),
|
|
endRange: z.number().int().min(1).max(254).optional(),
|
|
}).parse(req.body);
|
|
|
|
const discoveredDevices = await discoveryService.scanIpRange(baseIp, startRange, endRange);
|
|
res.json({ devices: discoveredDevices, count: discoveredDevices.length });
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
return res.status(400).json({
|
|
error: 'ValidationError',
|
|
details: error.errors.map((e) => ({ field: e.path.join('.'), message: e.message })),
|
|
});
|
|
}
|
|
console.error('Error scanning IP range:', error);
|
|
res.status(500).json({ error: 'InternalError', message: 'Failed to scan IP range' });
|
|
}
|
|
});
|
|
|
|
// POST /api/devices/discover/verify
|
|
router.post('/discover/verify', async (req: Request, res: Response) => {
|
|
try {
|
|
const { ipAddress, port } = z.object({
|
|
ipAddress: z.string().min(1, 'IP address is required'),
|
|
port: z.number().int().positive().optional(),
|
|
}).parse(req.body);
|
|
|
|
const device = await discoveryService.verifyDevice(ipAddress, port);
|
|
if (device) {
|
|
res.json({ found: true, device });
|
|
} else {
|
|
res.json({ found: false, message: 'No WLED device found at this address' });
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
return res.status(400).json({
|
|
error: 'ValidationError',
|
|
details: error.errors.map((e) => ({ field: e.path.join('.'), message: e.message })),
|
|
});
|
|
}
|
|
console.error('Error verifying device:', error);
|
|
res.status(500).json({ error: 'InternalError', message: 'Failed to verify device' });
|
|
}
|
|
});
|
|
|
|
export default router;
|