diff --git a/backend/dlbgatekeeper.service b/backend/dlbgatekeeper.service deleted file mode 100644 index 1c0f664..0000000 --- a/backend/dlbgatekeeper.service +++ /dev/null @@ -1,19 +0,0 @@ -[Unit] -Description=DLB Gate Keeper Service -After=network.target - -[Service] -Type=simple -User=gatekeeper -Group=gpio -WorkingDirectory=/home/gatekeeper/gatekeeper -Environment="RELAY_1_PIN=22" -Environment="RELAY_2_PIN=5" -Environment="STATUS_PIN=4" -Environment="TRIGGER_DURATION=500" -ExecStart=/usr/local/bin/uvicorn main:app --host 0.0.0.0 --port 3000 -Restart=always -RestartSec=3 - -[Install] -WantedBy=multi-user.target diff --git a/backend/main.py b/backend/main.py index a22f1d9..249cdae 100644 --- a/backend/main.py +++ b/backend/main.py @@ -6,10 +6,13 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from pydantic import BaseModel import aiosqlite -from datetime import datetime +from datetime import datetime, timedelta import asyncio from typing import List, Optional import logging +import json +from mqtt_integration import HomeAssistantMQTT + # Configure logging logging.basicConfig( @@ -20,6 +23,7 @@ logger = logging.getLogger(__name__) # Initialize FastAPI app = FastAPI() +ha_mqtt = HomeAssistantMQTT() # CORS middleware app.add_middleware( @@ -36,6 +40,7 @@ RELAY_1_PIN = int(os.getenv("RELAY_1_PIN", "22")) # GPIO22 (Pin 15) RELAY_2_PIN = int(os.getenv("RELAY_2_PIN", "5")) # GPIO5 (Pin 29) STATUS_PIN = int(os.getenv("STATUS_PIN", "4")) # GPIO4 (Pin 7) TRIGGER_DURATION = int(os.getenv("TRIGGER_DURATION", "500")) # 500ms default +DEFAULT_MAX_OPEN_TIME = 300 # seconds (5 minutes) # Models class GateEvent(BaseModel): @@ -46,8 +51,9 @@ class GateEvent(BaseModel): success: bool class Settings(BaseModel): - maxOpenTimeSeconds: str # Open time in seconds - triggerDuration: str # Trigger duration in milliseconds + maxOpenTimeSeconds: str + triggerDuration: str + mqtt: dict class GateStatus(BaseModel): isOpen: bool @@ -72,7 +78,9 @@ def setup_gpio(): # Database functions async def init_db(): + """Initialize the SQLite database""" async with aiosqlite.connect(DB_PATH) as db: + # Create events table await db.execute(""" CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -82,23 +90,43 @@ async def init_db(): success BOOLEAN NOT NULL ) """) - await db.execute(""" - CREATE TABLE IF NOT EXISTS settings ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - max_open_time TEXT NOT NULL, - trigger_duration TEXT NOT NULL - ) - """) + + # Create gate_status table await db.execute(""" CREATE TABLE IF NOT EXISTS gate_status ( + id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL ) """) - # Insert default settings if they don't exist + + # Create settings table await db.execute(""" - INSERT OR IGNORE INTO settings (max_open_time, trigger_duration) VALUES - ('300000', '500') + CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) """) + + # Insert default settings if they don't exist + default_settings = { + "maxOpenTimeSeconds": "300", + "triggerDuration": "500", + "mqtt": { + "broker": "localhost", + "port": "1883", + "username": "", + "password": "", + "clientId": "gatekeeper", + "enabled": False + } + } + + for key, value in default_settings.items(): + await db.execute( + "INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", + (key, json.dumps(value)) + ) + await db.commit() # Gate control @@ -112,36 +140,100 @@ async def trigger_gate() -> bool: logger.error(f"Error triggering gate: {e}") return False -async def update_gate_status(): - """Monitor gate status and update database when it changes""" - last_status = None +last_open_time = None + +async def check_auto_close(): + """Check if gate has been open too long and close it if needed""" + global last_open_time + while True: + try: + if GPIO.input(STATUS_PIN) == GPIO.HIGH: # Gate is open + current_time = datetime.now() + + # Initialize last_open_time if gate is open and time not set + if last_open_time is None: + last_open_time = current_time + + # Get max open time from settings + async with aiosqlite.connect(DB_PATH) as db: + cursor = await db.execute("SELECT value FROM settings WHERE key = 'maxOpenTimeSeconds'") + row = await cursor.fetchone() + max_open_time = int(json.loads(row[0])) if row else DEFAULT_MAX_OPEN_TIME + + # Check if gate has been open too long + if (current_time - last_open_time).total_seconds() > max_open_time: + logger.warning(f"Gate has been open for more than {max_open_time} seconds. Auto-closing...") + await trigger_gate() + timestamp = current_time.isoformat() + + # Log auto-close event + async with aiosqlite.connect(DB_PATH) as db: + await db.execute( + "INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)", + (timestamp, "auto-close", "system", True) + ) + await db.commit() + else: + # Reset last_open_time when gate is closed + last_open_time = None + + except Exception as e: + logger.error(f"Error in auto-close check: {e}") + + await asyncio.sleep(1) # Check every second + +async def update_gate_status(): + """Monitor gate status and update database when it changes""" + global last_open_time + last_status = None + while True: try: - # HIGH (3.3V) = OPEN, LOW (0V) = CLOSED current_status = GPIO.input(STATUS_PIN) == GPIO.HIGH # True = OPEN, False = CLOSED - if current_status != last_status: - timestamp = datetime.now().isoformat() + + if last_status != current_status: + timestamp = datetime.now() + + # Update last_open_time when gate opens + if current_status: # Gate just opened + last_open_time = timestamp + else: # Gate just closed + last_open_time = None + + # Update gate_status table async with aiosqlite.connect(DB_PATH) as db: await db.execute( "INSERT INTO gate_status (timestamp) VALUES (?)", - (timestamp,) + (timestamp.isoformat(),) ) - await db.commit() # Log the status change as an event status_str = "opened" if current_status else "closed" await db.execute( "INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)", - (timestamp, f"gate {status_str}", "sensor", True) + (timestamp.isoformat(), f"gate {status_str}", "sensor", True) ) await db.commit() + # Update Home Assistant via MQTT + await ha_mqtt.publish_state(current_status) last_status = current_status logger.info(f"Gate status changed to: {'open' if current_status else 'closed'}") + + await asyncio.sleep(0.5) # Check every 500ms + except Exception as e: - logger.error(f"Error monitoring gate status: {e}") - - await asyncio.sleep(0.1) # Check every 100ms + logger.error(f"Error in update_gate_status: {e}") + await asyncio.sleep(5) # Wait longer on error + +# MQQT Command Handler +async def handle_mqtt_command(should_open: bool): + """Handle commands received from Home Assistant""" + try: + if should_open != (GPIO.input(STATUS_PIN) == GPIO.HIGH): + await trigger_gate() + except Exception as e: + logger.error(f"Error handling MQTT command: {e}") # API Routes @app.post("/api/trigger") @@ -162,61 +254,81 @@ async def trigger(): @app.get("/api/status") async def get_status(): - # HIGH (3.3V) = OPEN, LOW (0V) = CLOSED - current_status = GPIO.input(STATUS_PIN) == GPIO.HIGH # True = OPEN, False = CLOSED + """Get current gate status""" + is_open = GPIO.input(STATUS_PIN) == GPIO.HIGH - # Get the last status change time + # Get last status change time async with aiosqlite.connect(DB_PATH) as db: cursor = await db.execute( "SELECT timestamp FROM gate_status ORDER BY timestamp DESC LIMIT 1" ) - row = await cursor.fetchone() - last_changed = row[0] if row else datetime.now().isoformat() + result = await cursor.fetchone() + last_changed = result[0] if result else datetime.now().isoformat() return { - "isOpen": current_status, + "isOpen": is_open, "lastChanged": last_changed } @app.get("/api/events") async def get_events(limit: int = 10): + """Get recent gate events""" async with aiosqlite.connect(DB_PATH) as db: db.row_factory = aiosqlite.Row cursor = await db.execute( "SELECT * FROM events ORDER BY timestamp DESC LIMIT ?", (limit,) ) - rows = await cursor.fetchall() - return [dict(row) for row in rows] + events = await cursor.fetchall() + return [dict(event) for event in events] @app.get("/api/settings") async def get_settings(): + """Get current settings""" async with aiosqlite.connect(DB_PATH) as db: - cursor = await db.execute("SELECT max_open_time, trigger_duration FROM settings ORDER BY id DESC LIMIT 1") - row = await cursor.fetchone() - if row: - max_open_time_ms, trigger_duration = row - # Convert milliseconds to seconds for maxOpenTime - return {"maxOpenTimeSeconds": str(int(max_open_time_ms) // 1000), "triggerDuration": str(trigger_duration)} - return {"maxOpenTimeSeconds": "300", "triggerDuration": "500"} + cursor = await db.execute("SELECT key, value FROM settings") + rows = await cursor.fetchall() + settings = {} + for key, value in rows: + settings[key] = json.loads(value) + + return { + "maxOpenTimeSeconds": settings.get("maxOpenTimeSeconds", "300"), + "triggerDuration": settings.get("triggerDuration", "500"), + "mqtt": settings.get("mqtt", { + "broker": "localhost", + "port": "1883", + "username": "", + "password": "", + "clientId": "gatekeeper", + "enabled": False + }) + } @app.post("/api/settings") async def update_settings(settings: Settings): - try: - # Convert seconds to milliseconds for storage - max_open_time_ms = int(settings.maxOpenTimeSeconds) * 1000 - trigger_duration = int(settings.triggerDuration) - - async with aiosqlite.connect(DB_PATH) as db: + """Update settings""" + async with aiosqlite.connect(DB_PATH) as db: + # Update each setting + for key, value in settings.dict().items(): await db.execute( - "INSERT INTO settings (max_open_time, trigger_duration) VALUES (?, ?)", - (str(max_open_time_ms), str(trigger_duration)) + "INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", + (key, json.dumps(value)) ) - await db.commit() - return {"success": True} - except Exception as e: - logger.error(f"Error updating settings: {e}") - raise HTTPException(status_code=500, detail="Failed to update settings") + 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"] + + # Enable/disable MQTT based on settings + ha_mqtt.enable(settings.mqtt.get("enabled", False)) + + return {"success": True} # Serve static files app.mount("/", StaticFiles(directory="../public", html=True), name="static") @@ -224,13 +336,37 @@ app.mount("/", StaticFiles(directory="../public", html=True), name="static") # Background task for monitoring gate status @app.on_event("startup") async def startup_event(): - setup_gpio() + """Initialize the application on startup""" + # Initialize database await init_db() - asyncio.create_task(update_gate_status()) - logger.info("Application started successfully") + + # Setup GPIO + setup_gpio() + + # Start background tasks + app.state.status_task = asyncio.create_task(update_gate_status()) + app.state.auto_close_task = asyncio.create_task(check_auto_close()) + + # Initialize MQTT from settings + try: + settings = await get_settings() + if settings["mqtt"].get("enabled"): + ha_mqtt.enable(True) + except Exception as e: + logger.error(f"Failed to initialize MQTT: {e}") -# Shutdown event @app.on_event("shutdown") async def shutdown_event(): + """Clean up on shutdown""" + # Cancel background tasks + if hasattr(app.state, "status_task"): + app.state.status_task.cancel() + if hasattr(app.state, "auto_close_task"): + app.state.auto_close_task.cancel() + + # Disconnect MQTT + await ha_mqtt.disconnect() + + # Cleanup GPIO GPIO.cleanup() logger.info("Application shutdown complete") diff --git a/backend/mqtt_integration.py b/backend/mqtt_integration.py new file mode 100644 index 0000000..b3a4e0e --- /dev/null +++ b/backend/mqtt_integration.py @@ -0,0 +1,182 @@ +import os +import json +import asyncio +import logging +from gmqtt import Client as MQTTClient +from typing import Optional, Callable + +logger = logging.getLogger(__name__) + +class HomeAssistantMQTT: + def __init__(self): + # MQTT Configuration + self.broker = os.getenv("MQTT_BROKER", "localhost") + self.port = int(os.getenv("MQTT_PORT", "1883")) + self.username = os.getenv("MQTT_USERNAME", None) + self.password = os.getenv("MQTT_PASSWORD", None) + self.client_id = os.getenv("MQTT_CLIENT_ID", "gatekeeper") + + # Home Assistant MQTT topics + self.node_id = "gatekeeper" + self.object_id = "gate" + self.discovery_prefix = "homeassistant" + + self.state_topic = f"homeassistant/cover/{self.node_id}/{self.object_id}/state" + self.command_topic = f"homeassistant/cover/{self.node_id}/{self.object_id}/command" + self.config_topic = f"homeassistant/cover/{self.node_id}/{self.object_id}/config" + self.availability_topic = f"homeassistant/cover/{self.node_id}/{self.object_id}/availability" + + self.client: Optional[MQTTClient] = None + self.command_callback: Optional[Callable] = None + self._connected = False + self._reconnect_task: Optional[asyncio.Task] = None + self._enabled = False + + def enable(self, enabled: bool = True): + """Enable or disable MQTT integration""" + self._enabled = enabled + if enabled and not self._connected and not self._reconnect_task: + # Set up command handler if not already set + from main import handle_mqtt_command # Import here to avoid circular import + self.set_command_callback(handle_mqtt_command) + # Start reconnection + self._reconnect_task = asyncio.create_task(self._reconnect_loop()) + elif not enabled and self._reconnect_task: + self._reconnect_task.cancel() + self._reconnect_task = None + asyncio.create_task(self.disconnect()) + + async def _reconnect_loop(self): + """Continuously try to reconnect when connection is lost""" + while self._enabled: + try: + if not self._connected: + await self.connect() + await asyncio.sleep(5) # Wait before checking connection again + except asyncio.CancelledError: + break + except Exception as e: + logger.error(f"Reconnection attempt failed: {e}") + await asyncio.sleep(5) # Wait before retrying + + def on_connect(self, client, flags, rc, properties): + """Callback for when connection is established""" + logger.info("Connected to MQTT broker") + self._connected = True + asyncio.create_task(self._post_connect()) + + def on_message(self, client, topic, payload, qos, properties): + """Callback for when a message is received""" + if topic == self.command_topic and self.command_callback: + try: + command = payload.decode() + asyncio.create_task(self.command_callback(command)) + except Exception as e: + logger.error(f"Error processing command: {e}") + + def on_disconnect(self, client, packet, exc=None): + """Callback for when connection is lost""" + logger.info("Disconnected from MQTT broker") + self._connected = False + # Start reconnection if enabled + if self._enabled and not self._reconnect_task: + self._reconnect_task = asyncio.create_task(self._reconnect_loop()) + + async def _post_connect(self): + """Tasks to run after connection is established""" + try: + # Send Home Assistant discovery configuration + await self.publish_discovery_config() + + # Publish availability status + await self.publish(self.availability_topic, "online", retain=True) + + # Subscribe to command topic + await self.subscribe(self.command_topic) + except Exception as e: + logger.error(f"Error in post-connect tasks: {e}") + + async def connect(self): + """Connect to MQTT broker""" + try: + self.client = MQTTClient(self.client_id) + + # Set callbacks + self.client.on_connect = self.on_connect + self.client.on_message = self.on_message + self.client.on_disconnect = self.on_disconnect + + # Set credentials if provided + if self.username and self.password: + self.client.set_auth_credentials(self.username, self.password) + + # Connect to broker + await self.client.connect(self.broker, self.port) + logger.info("Initiating connection to MQTT broker") + except Exception as e: + logger.error(f"Failed to connect to MQTT broker: {e}") + raise + + async def disconnect(self): + """Disconnect from MQTT broker""" + if self.client and self._connected: + try: + await self.publish(self.availability_topic, "offline", retain=True) + await self.client.disconnect() + except Exception as e: + logger.error(f"Error during disconnect: {e}") + + async def publish(self, topic: str, payload: str, retain: bool = False): + """Publish a message to a topic""" + if self.client and self._connected: + try: + await self.client.publish(topic, payload, retain=retain) + except Exception as e: + logger.error(f"Failed to publish message: {e}") + + async def subscribe(self, topic: str): + """Subscribe to a topic""" + if self.client and self._connected: + try: + await self.client.subscribe([(topic, 0)]) + except Exception as e: + logger.error(f"Failed to subscribe to topic: {e}") + + async def publish_discovery_config(self): + """Publish Home Assistant MQTT discovery configuration""" + config = { + "name": "Gate", + "unique_id": f"{self.node_id}_{self.object_id}", + "device_class": "gate", + "command_topic": self.command_topic, + "state_topic": self.state_topic, + "availability_topic": self.availability_topic, + "payload_available": "online", + "payload_not_available": "offline", + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "state_open": "open", + "state_closed": "closed", + "state_opening": "opening", + "state_closing": "closing", + "device": { + "identifiers": [self.node_id], + "name": "Gate Keeper", + "model": "DLB Gate Controller", + "manufacturer": "Athena Networks", + } + } + + try: + await self.publish(self.config_topic, json.dumps(config), retain=True) + except Exception as e: + logger.error(f"Failed to publish discovery config: {e}") + + def set_command_callback(self, callback: Callable): + """Set callback for handling commands""" + self.command_callback = callback + + async def publish_state(self, state: str): + """Publish gate state""" + await self.publish(self.state_topic, state) diff --git a/backend/requirements.txt b/backend/requirements.txt index c9ff5aa..e4f3b91 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,6 +1,10 @@ -fastapi==0.109.0 -uvicorn==0.27.0 -RPi.GPIO==0.7.1 -python-dotenv==1.0.0 +fastapi==0.104.1 +uvicorn==0.24.0 aiosqlite==0.19.0 +RPi.GPIO==0.7.1 +gmqtt==0.6.12 +pydantic==2.5.2 python-multipart==0.0.6 +anyio>=3.7.1,<4.0.0 +starlette==0.27.0 +typing-extensions>=4.8.0 diff --git a/deploy.sh b/deploy.sh index 0a45c07..ccada4f 100755 --- a/deploy.sh +++ b/deploy.sh @@ -37,12 +37,6 @@ cd /home/gatekeeper sudo rm -rf /home/gatekeeper/gatekeeper sudo mkdir -p /home/gatekeeper/gatekeeper sudo tar xzf gatekeeper.tar.gz -C /home/gatekeeper/gatekeeper -# Copy backend files to root level for the service -sudo cp -r /home/gatekeeper/gatekeeper/backend/* /home/gatekeeper/ - -echo "=== Restarting services ===" -cd /home/gatekeeper -sudo systemctl restart dlbgatekeeper echo "=== Cleaning up ===" rm gatekeeper.tar.gz diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 58f941f..4da5e55 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,12 +1,22 @@ import React, { useState, useEffect } from 'react'; import { GateEvent, Settings, GateStatus } from './types'; import * as api from './api'; +import { SettingsDialog } from './components/SettingsDialog'; +import { Cog6ToothIcon } from '@heroicons/react/24/outline'; function App() { const [events, setEvents] = useState([]); const [settings, setSettings] = useState({ - maxOpenTimeSeconds: '300', // 5 minutes in seconds - triggerDuration: '500' // 500ms + maxOpenTimeSeconds: '300', + triggerDuration: '500', + mqtt: { + broker: 'localhost', + port: '1883', + username: '', + password: '', + clientId: 'gatekeeper', + enabled: false + } }); const [gateStatus, setGateStatus] = useState({ isOpen: false, lastChanged: new Date().toISOString() }); const [loading, setLoading] = useState(false); @@ -84,149 +94,81 @@ function App() { }; return ( -
-
-
+
+
+
-
- {/* Header */} -

Gate Control

- - {/* Gate Status */} -
-

Gate Status

-
-
-
- - {gateStatus.isOpen ? 'Open' : 'Closed'} +
+
+
+

Gate Control

+ +
+ + {/* Gate Status */} +
+

+ Status: + {gateStatus.isOpen ? "Open" : "Closed"} -

-
+

+

Last changed: {formatDate(gateStatus.lastChanged)} +

+
+ + {/* Gate Control Button */} +
+ +
+ + {/* Error Display */} + {error && ( +
+ {error} +
+ )} + + {/* Recent Events */} +
+

Recent Events

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

{formatDate(event.timestamp)}

+

{event.action}

+

Source: {event.source}

+
+ ))}
- - {/* Gate Controls */} -
- -
- - {/* Error Display */} - {error && ( -
- {error} -
- )} - - {/* Recent Events */} -
-

Recent Events

-
- {events.map((event, index) => ( -
-
-
-
- - {event.action} - -
- - {formatDate(event.timestamp)} - -
-
- Source: {event.source} -
-
- ))} -
-
- - {/* Settings */} -
- -
- - {/* Settings Modal */} - {isSettingsOpen && ( -
-
-

Settings

- -
-
- - setSettings(prev => ({ ...prev, maxOpenTimeSeconds: e.target.value }))} - className="w-full px-3 py-2 rounded-lg border border-white/50 bg-white/30 backdrop-blur-md - focus:outline-none focus:ring-2 focus:ring-macos-blue/50 text-macos-text shadow-lg" - /> -
- -
- - setSettings(prev => ({ ...prev, triggerDuration: e.target.value }))} - className="w-full px-3 py-2 rounded-lg border border-white/50 bg-white/30 backdrop-blur-md - focus:outline-none focus:ring-2 focus:ring-macos-blue/50 text-macos-text shadow-lg" - /> -
-
- -
- - -
-
-
- )}
+ + {/* Settings Dialog */} + { + setIsSettingsOpen(false); + // loadSettings(); // Refresh settings after dialog closes + }} + />
); } diff --git a/frontend/src/components/SettingsDialog.tsx b/frontend/src/components/SettingsDialog.tsx new file mode 100644 index 0000000..1f5ae37 --- /dev/null +++ b/frontend/src/components/SettingsDialog.tsx @@ -0,0 +1,237 @@ +import React, { useState, useEffect } from 'react'; +import { Settings, MQTTSettings } from '../types'; +import { getSettings, updateSettings } from '../api'; + +interface SettingsDialogProps { + isOpen: boolean; + onClose: () => void; +} + +export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { + const [settings, setSettings] = useState({ + maxOpenTimeSeconds: "300", + triggerDuration: "500", + mqtt: { + broker: "localhost", + port: "1883", + username: "", + password: "", + clientId: "gatekeeper", + enabled: false + } + }); + + useEffect(() => { + if (isOpen) { + loadSettings(); + } + }, [isOpen]); + + const loadSettings = async () => { + try { + const currentSettings = await getSettings(); + setSettings(currentSettings); + } catch (error) { + console.error('Failed to load settings:', error); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + await updateSettings(settings); + onClose(); + } catch (error) { + console.error('Failed to update settings:', error); + } + }; + + const handleMQTTChange = (e: React.ChangeEvent) => { + const { name, value, type, checked } = e.target; + setSettings(prev => ({ + ...prev, + mqtt: { + ...prev.mqtt, + [name]: type === 'checkbox' ? checked : value + } + })); + }; + + if (!isOpen) return null; + + return ( +
+
+
+

Settings

+ + {/* Gate Settings */} +
+

Gate Settings

+
+
+ + setSettings(prev => ({ ...prev, maxOpenTimeSeconds: e.target.value }))} + className="mt-1 block w-full rounded-lg border border-[#e5e5e5] bg-white/50 backdrop-blur-sm + px-3 py-2 text-[#1d1d1f] shadow-sm + focus:outline-none focus:ring-2 focus:ring-[#0071e3]/30 focus:border-[#0071e3] + transition-colors duration-200" + /> +
+
+ + setSettings(prev => ({ ...prev, triggerDuration: e.target.value }))} + className="mt-1 block w-full rounded-lg border border-[#e5e5e5] bg-white/50 backdrop-blur-sm + px-3 py-2 text-[#1d1d1f] shadow-sm + focus:outline-none focus:ring-2 focus:ring-[#0071e3]/30 focus:border-[#0071e3] + transition-colors duration-200" + /> +
+
+
+ + {/* MQTT Settings */} +
+

MQTT Settings

+
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + {/* Dialog Actions */} +
+ + +
+
+
+
+ ); +} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 604a13a..02c5cbf 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -6,9 +6,19 @@ export interface GateEvent { success: boolean; } +export interface MQTTSettings { + broker: string; + port: string; + username: string; + password: string; + clientId: string; + enabled: boolean; +} + export interface Settings { maxOpenTimeSeconds: string; // Open time in seconds triggerDuration: string; // Trigger duration in milliseconds + mqtt: MQTTSettings; } export interface GateStatus { diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8ab3e36 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.104.1 +uvicorn==0.24.0 +aiosqlite==0.19.0 +RPi.GPIO==0.7.1 +asyncio-mqtt==0.16.1