import { Router, Response } from 'express'; import { spawn } from 'child_process'; import fs from 'fs'; import path from 'path'; import { AuthedRequest, requireAuth, requireRole } from '../middleware/auth'; /** * Steuerung des Homematic-IP-Türschlosses (Access Point + App, kein * lokaler CCU-Zugriff). Da Homematic IP für dieses Setup keine offizielle * Partner-API anbietet, wird die aktiv gepflegte, quelloffene Community- * Bibliothek `homematicip` (Python) über zwei Skripte angesprochen * (backend/scripts/hmip_register.py, hmip_control.py) statt das * Cloud-Protokoll selbst nachzubauen. * * Echte Steuerung ist erst nutzbar, wenn HMIP_ACCESS_POINT/HMIP_AUTH_TOKEN * gesetzt sind (per einmaliger Kopplung über /smartlock/pairing/*, dabei * muss der blaue Knopf am Access Point physisch gedrückt werden). */ export const smartLockRouter = Router(); const SCRIPTS_DIR = path.join(__dirname, '..', '..', 'scripts'); const PAIRING_STATUS_FILE = path.join('/tmp', 'hmip_pairing_status.json'); function runPythonScript(script: string, args: string[], env: NodeJS.ProcessEnv = process.env): Promise<{ stdout: string; stderr: string; code: number }> { return new Promise((resolve) => { const proc = spawn('python3', [path.join(SCRIPTS_DIR, script), ...args], { env }); let stdout = ''; let stderr = ''; proc.stdout.on('data', (d) => (stdout += d.toString())); proc.stderr.on('data', (d) => (stderr += d.toString())); proc.on('close', (code) => resolve({ stdout, stderr, code: code ?? 1 })); }); } function hmipConfigured(): boolean { return Boolean(process.env.HMIP_ACCESS_POINT && process.env.HMIP_AUTH_TOKEN); } // Startet die einmalige Kopplung (Landlord/Admin). Läuft im Hintergrund, // da bis zu ~6 Minuten auf den Knopfdruck am Access Point gewartet wird. smartLockRouter.post( '/smartlock/pairing/start', requireAuth, requireRole('LANDLORD', 'ADMIN'), async (req: AuthedRequest, res: Response) => { const { sgtin } = req.body || {}; if (!sgtin || typeof sgtin !== 'string') { return res.status(400).json({ error: 'sgtin ist erforderlich' }); } try { fs.writeFileSync(PAIRING_STATUS_FILE, JSON.stringify({ state: 'starting' })); } catch { return res.status(500).json({ error: 'Konnte Status-Datei nicht anlegen' }); } const proc = spawn('python3', [path.join(SCRIPTS_DIR, 'hmip_register.py'), sgtin, PAIRING_STATUS_FILE], { detached: true, stdio: 'ignore', }); proc.unref(); res.status(202).json({ message: 'Kopplung gestartet. Bitte jetzt den blauen Knopf am Access Point drücken (bis zu 6 Minuten Zeit). Status via GET /smartlock/pairing/status abfragen.' }); }, ); smartLockRouter.get( '/smartlock/pairing/status', requireAuth, requireRole('LANDLORD', 'ADMIN'), async (_req: AuthedRequest, res: Response) => { if (!fs.existsSync(PAIRING_STATUS_FILE)) { return res.status(200).json({ state: 'idle' }); } try { const data = JSON.parse(fs.readFileSync(PAIRING_STATUS_FILE, 'utf-8')); res.status(200).json(data); } catch { res.status(200).json({ state: 'idle' }); } }, ); smartLockRouter.get('/smartlock/status', requireAuth, async (_req: AuthedRequest, res: Response) => { res.status(200).json({ configured: hmipConfigured() }); }); smartLockRouter.get('/smartlock/devices', requireAuth, requireRole('LANDLORD', 'ADMIN'), async (_req: AuthedRequest, res: Response) => { if (!hmipConfigured()) { return res.status(409).json({ error: 'Homematic IP ist noch nicht gekoppelt (siehe /smartlock/pairing/start)' }); } const { stdout, stderr, code } = await runPythonScript('hmip_control.py', ['list-devices']); if (code !== 0) { return res.status(502).json({ error: 'Abfrage fehlgeschlagen', detail: stderr || stdout }); } try { res.status(200).json({ devices: JSON.parse(stdout) }); } catch { res.status(502).json({ error: 'Unerwartete Antwort von Homematic IP', detail: stdout }); } }); smartLockRouter.post( '/smartlock/devices/:deviceId/:action(lock|unlock)', requireAuth, async (req: AuthedRequest, res: Response) => { if (!hmipConfigured()) { return res.status(409).json({ error: 'Homematic IP ist noch nicht gekoppelt (siehe /smartlock/pairing/start)' }); } const { deviceId, action } = req.params; const { stdout, stderr, code } = await runPythonScript('hmip_control.py', [action, deviceId]); if (code !== 0) { return res.status(502).json({ error: 'Türsteuerung fehlgeschlagen', detail: stderr || stdout }); } res.status(200).json({ ok: true }); }, );