diff --git a/modelPics/S3PL-00112EU.png b/modelPics/S3PL-00112EU.png new file mode 100644 index 0000000..99a31b6 Binary files /dev/null and b/modelPics/S3PL-00112EU.png differ diff --git a/modelPics/S3SW-001P16EU.png b/modelPics/S3SW-001P16EU.png new file mode 100644 index 0000000..4158d5b Binary files /dev/null and b/modelPics/S3SW-001P16EU.png differ diff --git a/modelPics/S3SW-001X8EU.png b/modelPics/S3SW-001X8EU.png new file mode 100644 index 0000000..d6c1b4c Binary files /dev/null and b/modelPics/S3SW-001X8EU.png differ diff --git a/modelPics/S3SW-002P16EU.png b/modelPics/S3SW-002P16EU.png new file mode 100644 index 0000000..195c3c9 Binary files /dev/null and b/modelPics/S3SW-002P16EU.png differ diff --git a/modelPics/SNDC-0D4P10WW.png b/modelPics/SNDC-0D4P10WW.png new file mode 100644 index 0000000..f6266f0 Binary files /dev/null and b/modelPics/SNDC-0D4P10WW.png differ diff --git a/modelPics/SNDM-00100WW.png b/modelPics/SNDM-00100WW.png new file mode 100644 index 0000000..b919f64 Binary files /dev/null and b/modelPics/SNDM-00100WW.png differ diff --git a/server.js b/server.js index a5eb5d2..01840be 100644 --- a/server.js +++ b/server.js @@ -4,6 +4,7 @@ import path from 'path'; import sqlite3 from 'sqlite3'; import { fileURLToPath } from 'url'; import { initRuleEngine, loadRules, runRules, watchRules } from './rule_engine.js'; +import { broadcastEvent, startStatusServer } from './status_server.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -143,6 +144,7 @@ function checkAndLogEvent(mac, component, field, type, event, connectionId = nul if (connectionId) console.log(`[ID: ${connectionId}] Event logged: ${event} on ${component} (${field})`); db.run("INSERT INTO events (channel_id, event, timestamp) VALUES (?, ?, ?)", [channelId, String(event), new Date().toISOString()]); forwardToUpstream(); + broadcastEvent(mac, component, field, type, event); runRules(mac, component, field, type, event); return; } @@ -160,6 +162,7 @@ function checkAndLogEvent(mac, component, field, type, event, connectionId = nul if (connectionId) console.log(`[ID: ${connectionId}] Status change logged: ${event} on ${component} (${field})`); db.run("INSERT INTO events (channel_id, event, timestamp) VALUES (?, ?, ?)", [channelId, currentEventStr, new Date().toISOString()]); forwardToUpstream(); + broadcastEvent(mac, component, field, type, currentEventStr); runRules(mac, component, field, type, currentEventStr); } }); @@ -179,6 +182,9 @@ loadRules().then(() => { console.error('Error loading rules:', err); }); +// Start status dashboard server +startStatusServer(); + // Global counter for connection IDs let connectionIdCounter = 0; @@ -349,11 +355,17 @@ wss.on('connection', (ws, req) => { if (data.method === 'NotifyEvent') { if (data.params && data.params.events) { const mac = connectionDeviceMap.get(connectionId); - // Even if we don't have MAC from map yet (unlikely for identified device), we can try data.src or skip + // Known button event types to store + const knownButtonEvents = ['single_push', 'double_push', 'triple_push', 'long_push', 'btn_down', 'btn_up']; + if (mac) { data.params.events.forEach(evt => { - // Pass the button event (btn_down/up/etc) as values - checkAndLogEvent(mac, evt.component, 'button', 'enum', evt.event, connectionId); + // Only store known input button events + if (evt.component.startsWith('input') && knownButtonEvents.includes(evt.event)) { + checkAndLogEvent(mac, evt.component, 'button', 'enum', evt.event, connectionId); + } else { + console.log(`[ID: ${connectionId}] Skipped unknown event: ${evt.component} -> ${evt.event}`); + } }); } } diff --git a/status_server.js b/status_server.js new file mode 100644 index 0000000..249eddb --- /dev/null +++ b/status_server.js @@ -0,0 +1,583 @@ +import http from 'http'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { WebSocketServer } from 'ws'; +import sqlite3 from 'sqlite3'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const PORT = 8081; +const MODEL_PICS_DIR = path.join(__dirname, 'modelPics'); + +// SQLite database connection (read-only for status queries) +const db = new sqlite3.Database('devices.db', sqlite3.OPEN_READONLY); + +// WebSocket clients for broadcasting +const wsClients = new Set(); + +// Query initial status data +function getStatusData() { + return new Promise((resolve, reject) => { + const sql = ` + SELECT d.mac, d.model, d.connected, d.last_seen, + c.id as channel_id, c.component, c.field, c.type, + (SELECT e.event FROM events e WHERE e.channel_id = c.id ORDER BY e.id DESC LIMIT 1) as last_event, + (SELECT e.timestamp FROM events e WHERE e.channel_id = c.id ORDER BY e.id DESC LIMIT 1) as last_ts + FROM devices d + LEFT JOIN channels c ON c.mac = d.mac + ORDER BY d.mac, c.component, c.field + `; + db.all(sql, [], (err, rows) => { + if (err) reject(err); + else { + // Group by device + const devices = {}; + for (const row of rows) { + if (!devices[row.mac]) { + devices[row.mac] = { + mac: row.mac, + model: row.model, + connected: row.connected, + last_seen: row.last_seen, + channels: [] + }; + } + if (row.channel_id) { + devices[row.mac].channels.push({ + id: row.channel_id, + component: row.component, + field: row.field, + type: row.type, + event: row.last_event, + timestamp: row.last_ts + }); + } + } + resolve(Object.values(devices)); + } + }); + }); +} + +// Broadcast event to all connected WebSocket clients +export function broadcastEvent(mac, component, field, type, event) { + const message = JSON.stringify({ + type: 'event', + mac, + component, + field, + eventType: type, + event, + timestamp: new Date().toISOString() + }); + for (const client of wsClients) { + if (client.readyState === 1) { // OPEN + client.send(message); + } + } +} + +// HTML Dashboard +const dashboardHTML = ` + +
+ + ++ + Connecting... +
+Loading devices...
+