From f6ca88f012b97535e57d2cab9b1d48a9705afd4d Mon Sep 17 00:00:00 2001 From: Josh Finlay Date: Wed, 8 Jan 2025 07:46:08 +1000 Subject: [PATCH] feat: Add MQTT and settings event logging, implement event pagination - Add event logging for MQTT connection states - Add event logging for settings changes - Make event list more compact - Implement pagination for event list with 'Show More' button --- backend/main.py | 115 ++++++++++++++++++++++++++++-------- backend/mqtt_integration.py | 16 +++++ frontend/src/App.tsx | 51 ++++++++++++---- frontend/src/api.ts | 6 +- 4 files changed, 150 insertions(+), 38 deletions(-) diff --git a/backend/main.py b/backend/main.py index 249cdae..6274edb 100644 --- a/backend/main.py +++ b/backend/main.py @@ -25,6 +25,18 @@ logger = logging.getLogger(__name__) app = FastAPI() ha_mqtt = HomeAssistantMQTT() +# Set up MQTT event logging +async def log_mqtt_event(action: str, success: bool = True): + """Log MQTT events to the database""" + async with aiosqlite.connect(DB_PATH) as db: + await db.execute( + "INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)", + (datetime.utcnow().isoformat(), action, "MQTT", success) + ) + await db.commit() + +ha_mqtt.set_event_callback(log_mqtt_event) + # CORS middleware app.add_middleware( CORSMiddleware, @@ -271,16 +283,31 @@ async def get_status(): } @app.get("/api/events") -async def get_events(limit: int = 10): - """Get recent gate events""" +async def get_events(limit: int = 10, offset: int = 0): + """Get recent gate events with pagination""" async with aiosqlite.connect(DB_PATH) as db: db.row_factory = aiosqlite.Row + + # Get total count + cursor = await db.execute("SELECT COUNT(*) as count FROM events") + row = await cursor.fetchone() + total_count = row['count'] + + # Get paginated events cursor = await db.execute( - "SELECT * FROM events ORDER BY timestamp DESC LIMIT ?", - (limit,) + """ + SELECT * FROM events + ORDER BY timestamp DESC + LIMIT ? OFFSET ? + """, + (limit, offset) ) events = await cursor.fetchall() - return [dict(event) for event in events] + return { + "events": [dict(event) for event in events], + "total": total_count, + "hasMore": (offset + limit) < total_count + } @app.get("/api/settings") async def get_settings(): @@ -308,27 +335,65 @@ async def get_settings(): @app.post("/api/settings") async def update_settings(settings: Settings): """Update settings""" - async with aiosqlite.connect(DB_PATH) as db: - # Update each setting - for key, value in settings.dict().items(): - await db.execute( - "INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", - (key, json.dumps(value)) - ) - await db.commit() - - # Update environment variables and MQTT connection - if settings.mqtt: - os.environ["MQTT_BROKER"] = settings.mqtt["broker"] - os.environ["MQTT_PORT"] = settings.mqtt["port"] - os.environ["MQTT_USERNAME"] = settings.mqtt["username"] - os.environ["MQTT_PASSWORD"] = settings.mqtt["password"] - os.environ["MQTT_CLIENT_ID"] = settings.mqtt["clientId"] + try: + async with aiosqlite.connect(DB_PATH) as db: + # Update each setting + for key, value in settings.dict().items(): + await db.execute( + "INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", + (key, json.dumps(value)) + ) + await db.commit() - # Enable/disable MQTT based on settings - ha_mqtt.enable(settings.mqtt.get("enabled", False)) - - return {"success": True} + # Update environment variables and MQTT connection + if settings.mqtt: + os.environ["MQTT_BROKER"] = settings.mqtt["broker"] + os.environ["MQTT_PORT"] = settings.mqtt["port"] + os.environ["MQTT_USERNAME"] = settings.mqtt["username"] + os.environ["MQTT_PASSWORD"] = settings.mqtt["password"] + os.environ["MQTT_CLIENT_ID"] = settings.mqtt["clientId"] + + # Enable/disable MQTT based on settings + ha_mqtt.enable(settings.mqtt.get("enabled", False)) + + # Log settings update event with details + changes = [] + if settings.maxOpenTimeSeconds: + changes.append(f"Max Open Time: {settings.maxOpenTimeSeconds}s") + if settings.triggerDuration: + changes.append(f"Trigger Duration: {settings.triggerDuration}ms") + if settings.mqtt: + mqtt_status = "Enabled" if settings.mqtt.get("enabled") else "Disabled" + changes.append(f"MQTT: {mqtt_status}") + if settings.mqtt.get("enabled"): + changes.append(f"Broker: {settings.mqtt['broker']}:{settings.mqtt['port']}") + + await db.execute( + "INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)", + ( + datetime.utcnow().isoformat(), + f"Settings Updated ({'; '.join(changes)})", + "Settings", + True + ) + ) + await db.commit() + + return {"success": True} + except Exception as e: + # Log failure event + async with aiosqlite.connect(DB_PATH) as db: + await db.execute( + "INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)", + ( + datetime.utcnow().isoformat(), + f"Settings Update Failed: {str(e)}", + "Settings", + False + ) + ) + await db.commit() + raise HTTPException(status_code=500, detail=str(e)) # Serve static files app.mount("/", StaticFiles(directory="../public", html=True), name="static") diff --git a/backend/mqtt_integration.py b/backend/mqtt_integration.py index b3a4e0e..b4ed75b 100644 --- a/backend/mqtt_integration.py +++ b/backend/mqtt_integration.py @@ -31,6 +31,16 @@ class HomeAssistantMQTT: self._connected = False self._reconnect_task: Optional[asyncio.Task] = None self._enabled = False + self._event_callback: Optional[Callable] = None + + def set_event_callback(self, callback: Callable): + """Set callback for logging events""" + self._event_callback = callback + + async def _log_event(self, action: str, success: bool = True): + """Log MQTT events if callback is set""" + if self._event_callback: + await self._event_callback(action, success) def enable(self, enabled: bool = True): """Enable or disable MQTT integration""" @@ -41,10 +51,12 @@ class HomeAssistantMQTT: self.set_command_callback(handle_mqtt_command) # Start reconnection self._reconnect_task = asyncio.create_task(self._reconnect_loop()) + asyncio.create_task(self._log_event("MQTT Enabled")) elif not enabled and self._reconnect_task: self._reconnect_task.cancel() self._reconnect_task = None asyncio.create_task(self.disconnect()) + asyncio.create_task(self._log_event("MQTT Disabled")) async def _reconnect_loop(self): """Continuously try to reconnect when connection is lost""" @@ -63,6 +75,7 @@ class HomeAssistantMQTT: """Callback for when connection is established""" logger.info("Connected to MQTT broker") self._connected = True + asyncio.create_task(self._log_event("MQTT Connected")) asyncio.create_task(self._post_connect()) def on_message(self, client, topic, payload, qos, properties): @@ -78,6 +91,8 @@ class HomeAssistantMQTT: """Callback for when connection is lost""" logger.info("Disconnected from MQTT broker") self._connected = False + # Log disconnect event + asyncio.create_task(self._log_event("MQTT Disconnected")) # Start reconnection if enabled if self._enabled and not self._reconnect_task: self._reconnect_task = asyncio.create_task(self._reconnect_loop()) @@ -115,6 +130,7 @@ class HomeAssistantMQTT: logger.info("Initiating connection to MQTT broker") except Exception as e: logger.error(f"Failed to connect to MQTT broker: {e}") + await self._log_event("MQTT Connection Failed", success=False) raise async def disconnect(self): diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4da5e55..ba13d76 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,6 +6,8 @@ import { Cog6ToothIcon } from '@heroicons/react/24/outline'; function App() { const [events, setEvents] = useState([]); + const [hasMoreEvents, setHasMoreEvents] = useState(false); + const [totalEvents, setTotalEvents] = useState(0); const [settings, setSettings] = useState({ maxOpenTimeSeconds: '300', triggerDuration: '500', @@ -36,7 +38,9 @@ function App() { api.getSettings(), api.getGateStatus() ]); - setEvents(eventsData); + setEvents(eventsData.events); + setHasMoreEvents(eventsData.hasMore); + setTotalEvents(eventsData.total); setSettings(settingsData); setGateStatus(statusData); } catch (err) { @@ -53,7 +57,9 @@ function App() { const status = await api.getGateStatus(); setGateStatus(status); const newEvents = await api.getEvents(); - setEvents(newEvents); + setEvents(newEvents.events); + setHasMoreEvents(newEvents.hasMore); + setTotalEvents(newEvents.total); } catch (err) { console.error('Failed to update gate status:', err); } @@ -68,7 +74,7 @@ function App() { const result = await api.triggerGate(); if (result.success) { const newEvents = await api.getEvents(); - setEvents(newEvents); + setEvents(newEvents.events); setGateStatus(prev => ({ ...prev, isOpen: result.currentStatus })); } } catch (err) { @@ -93,6 +99,18 @@ function App() { } }; + const handleLoadMore = async () => { + try { + const moreEvents = await api.getEvents(10, events.length); + setEvents(prev => [...prev, ...moreEvents.events]); + setHasMoreEvents(moreEvents.hasMore); + setTotalEvents(moreEvents.total); + } catch (err) { + setError('Failed to load more events'); + console.error(err); + } + }; + return (
@@ -143,17 +161,30 @@ function App() { )} {/* Recent Events */} -
-

Recent Events

-
+
+

Recent Events

+
{events.map((event, index) => ( -
-

{formatDate(event.timestamp)}

-

{event.action}

-

Source: {event.source}

+
+
+
+ {event.action} + ({event.source}) +
+
{formatDate(event.timestamp)}
+
+
))}
+ {hasMoreEvents && ( + + )}
diff --git a/frontend/src/api.ts b/frontend/src/api.ts index ce7eb70..1d34c2f 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -20,10 +20,10 @@ export async function getGateStatus(): Promise { return response.json(); } -export async function getEvents(limit: number = 10): Promise { - const response = await fetch(`${API_BASE}/events?limit=${limit}`); +export async function getEvents(limit: number = 10, offset: number = 0): Promise<{ events: GateEvent[], total: number, hasMore: boolean }> { + const response = await fetch(`${API_BASE}/events?limit=${limit}&offset=${offset}`); if (!response.ok) { - throw new Error(`Failed to get events: ${response.statusText}`); + throw new Error('Failed to fetch events'); } return response.json(); }