diff --git a/uiserver/webpack.config.js b/uiserver/webpack.config.js index 80f8941..55bd0e4 100644 --- a/uiserver/webpack.config.js +++ b/uiserver/webpack.config.js @@ -22,6 +22,13 @@ try { console.error(`[UI Server] Failed to connect to database at ${dbPath}:`, err.message); } +// Output bindings: map virtual outputs to physical devices +// Format: outputChannel -> { device, channel, type } +const OUTPUT_BINDINGS = { + 'BigDehumid': { device: 'tapo', channel: 'r0', type: 'switch' }, + 'CO2Valve': { device: 'tapo', channel: 'c', type: 'switch' }, +}; + // ============================================= // WebSocket Server for Agents (port 3962) // ============================================= @@ -217,6 +224,11 @@ function handleAgentMessage(ws, message, clientState, clientId) { ws.send(JSON.stringify({ type: 'auth', success: true, devicePrefix: keyInfo.device_prefix, name: keyInfo.name })); break; + case 'pong': + // Keepalive from agent - just update timestamp + clientState.lastPong = Date.now(); + break; + case 'data': if (!clientState.authenticated) { ws.send(JSON.stringify({ type: 'error', error: 'Not authenticated' })); @@ -266,6 +278,45 @@ function sendCommandToDevicePrefix(devicePrefix, command) { return sent > 0; } +// Periodic sync: push non-zero output states to agents every 60s +function syncOutputStates() { + if (!db) return; + + try { + // Get current output values + const stmt = db.prepare(` + SELECT channel, value FROM output_events + WHERE id IN (SELECT MAX(id) FROM output_events GROUP BY channel) + `); + const rows = stmt.all(); + + for (const row of rows) { + // Only sync non-zero values + if (row.value > 0) { + const binding = OUTPUT_BINDINGS[row.channel]; + if (binding) { + const success = sendCommandToDevicePrefix(`${binding.device}:`, { + device: binding.channel, + action: 'set_state', + value: 1 + }); + + if (!success) { + console.error(`[Sync] ERROR: Cannot deliver 'on' command for ${row.channel} -> ${binding.device}:${binding.channel} (no agent connected)`); + } + } + } + } + } catch (err) { + console.error('[Sync] Error syncing output states:', err.message); + } +} + +// Start output state sync interval (every 60s) +setInterval(syncOutputStates, 60000); +// Also sync immediately on startup after a short delay +setTimeout(syncOutputStates, 5000); + // Start the WebSocket server const agentWss = createAgentWebSocketServer(); @@ -515,13 +566,6 @@ module.exports = { } }); - // Output bindings: map virtual outputs to physical devices - // Format: outputChannel -> { device, channel, type } - const OUTPUT_BINDINGS = { - 'BigDehumid': { device: 'tapo', channel: 'r0', type: 'switch' }, - 'CO2Valve': { device: 'tapo', channel: 'c', type: 'switch' }, - }; - // GET /api/outputs/commands - Get desired states for bound devices // Agents poll this to get commands. Returns { "device:channel": { state: 0|1 } } app.get('/api/outputs/commands', (req, res) => {