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
This commit is contained in:
parent
70f7e4fd84
commit
f6ca88f012
|
|
@ -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,6 +335,7 @@ async def get_settings():
|
|||
@app.post("/api/settings")
|
||||
async def update_settings(settings: Settings):
|
||||
"""Update settings"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
# Update each setting
|
||||
for key, value in settings.dict().items():
|
||||
|
|
@ -328,7 +356,44 @@ async def update_settings(settings: Settings):
|
|||
# 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")
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { Cog6ToothIcon } from '@heroicons/react/24/outline';
|
|||
|
||||
function App() {
|
||||
const [events, setEvents] = useState<GateEvent[]>([]);
|
||||
const [hasMoreEvents, setHasMoreEvents] = useState(false);
|
||||
const [totalEvents, setTotalEvents] = useState(0);
|
||||
const [settings, setSettings] = useState<Settings>({
|
||||
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 (
|
||||
<div className="min-h-screen bg-gray-100 py-6 flex flex-col justify-center sm:py-12">
|
||||
<div className="relative py-3 sm:max-w-xl sm:mx-auto">
|
||||
|
|
@ -143,17 +161,30 @@ function App() {
|
|||
)}
|
||||
|
||||
{/* Recent Events */}
|
||||
<div className="mt-8">
|
||||
<h2 className="text-xl font-semibold mb-4">Recent Events</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="mt-6">
|
||||
<h2 className="text-lg font-semibold mb-2">Recent Events</h2>
|
||||
<div className="border rounded-lg divide-y divide-gray-200 max-h-48 overflow-y-auto">
|
||||
{events.map((event, index) => (
|
||||
<div key={index} className="p-4 bg-gray-50 rounded-lg">
|
||||
<p className="text-sm text-gray-600">{formatDate(event.timestamp)}</p>
|
||||
<p className="font-medium">{event.action}</p>
|
||||
<p className="text-sm text-gray-500">Source: {event.source}</p>
|
||||
<div key={index} className="px-3 py-2 hover:bg-gray-50 flex items-center justify-between text-sm">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{event.action}</span>
|
||||
<span className="text-xs text-gray-500">({event.source})</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{formatDate(event.timestamp)}</div>
|
||||
</div>
|
||||
<div className={`w-2 h-2 rounded-full ${event.success ? 'bg-green-500' : 'bg-red-500'}`} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{hasMoreEvents && (
|
||||
<button
|
||||
onClick={handleLoadMore}
|
||||
className="mt-2 w-full text-sm text-gray-600 hover:text-gray-900 py-1 border rounded-md hover:bg-gray-50"
|
||||
>
|
||||
Show More ({events.length} of {totalEvents})
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@ export async function getGateStatus(): Promise<GateStatus> {
|
|||
return response.json();
|
||||
}
|
||||
|
||||
export async function getEvents(limit: number = 10): Promise<GateEvent[]> {
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue