refactor: improve database operations and settings management

- Added dedicated database operation functions
- Improved settings validation and updates
- Added proper state machine for gate status
- Added MQTT error handling
- Added startup state validation
- Fixed partial settings updates
This commit is contained in:
Josh Finlay 2025-01-08 09:11:39 +10:00
parent a3ecf3a606
commit 9046cbca1d
1 changed files with 368 additions and 229 deletions

View File

@ -13,6 +13,7 @@ from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional, Dict, Any, List, Union from typing import Optional, Dict, Any, List, Union
import RPi.GPIO as GPIO import RPi.GPIO as GPIO
from enum import Enum, auto
from mqtt_integration import HomeAssistantMQTT from mqtt_integration import HomeAssistantMQTT
@ -37,8 +38,8 @@ class MQTTSettings(BaseModel):
enabled: bool = False enabled: bool = False
class GPIOSettings(BaseModel): class GPIOSettings(BaseModel):
gatePin: int = 17 # Default GPIO pin for gate control gatePin: int = 15 # Relay control pin
statusPin: int = 27 # Default GPIO pin for gate status statusPin: int = 7 # Gate open status pin
class LoggingSettings(BaseModel): class LoggingSettings(BaseModel):
level: str = "WARNING" # Default to WARNING level level: str = "WARNING" # Default to WARNING level
@ -52,9 +53,43 @@ class Settings(BaseModel):
gpio: GPIOSettings = GPIOSettings() gpio: GPIOSettings = GPIOSettings()
logging: LoggingSettings = LoggingSettings() logging: LoggingSettings = LoggingSettings()
class GateStatus(BaseModel): class GateState(Enum):
isOpen: bool """Gate state enumeration"""
lastChanged: str UNKNOWN = auto()
OPEN = auto()
CLOSED = auto()
TRANSITIONING = auto()
class GateStatus:
"""Gate status tracking"""
def __init__(self):
self.state = GateState.UNKNOWN
self.last_change = datetime.now()
self.transition_start = None
def update(self, is_open: bool) -> bool:
"""Update state and return True if state changed"""
now = datetime.now()
new_state = GateState.OPEN if is_open else GateState.CLOSED
if new_state != self.state:
self.state = new_state
self.last_change = now
return True
return False
@property
def is_open(self) -> bool:
"""Return True if gate is open"""
return self.state == GateState.OPEN
@property
def last_changed(self) -> str:
"""Return ISO formatted last change time"""
return self.last_change.isoformat()
# Initialize gate status
gate_status = GateStatus()
# Configure logging # Configure logging
def setup_logging(settings: Settings): def setup_logging(settings: Settings):
@ -121,16 +156,77 @@ def get_db():
"""Get a new database connection""" """Get a new database connection"""
return DBConnection() return DBConnection()
# Database operations
async def add_event(timestamp: str, action: str, source: str, success: bool = True):
"""Add an event to the database"""
async with get_db() as db:
await db.execute(
"INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)",
(timestamp, action, source, success)
)
await db.commit()
async def add_gate_status(timestamp: str):
"""Add a gate status change to the database"""
async with get_db() as db:
await db.execute(
"INSERT INTO gate_status (timestamp) VALUES (?)",
(timestamp,)
)
await db.commit()
async def update_settings(settings_update: Dict[str, Any]):
"""Update settings in the database"""
async with get_db() as db:
# Update each setting in the database
for key, value in settings_update.items():
await db.execute(
"INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)",
(key, json.dumps(value))
)
await db.commit()
async def get_settings():
"""Get current settings from the database"""
async with get_db() as db:
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 settings
async def get_events(limit: int = 10, offset: int = 0):
"""Get recent gate events with pagination"""
async with get_db() 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 ? OFFSET ?
""",
(limit, offset)
)
events = await cursor.fetchall()
return {
"events": [dict(event) for event in events],
"total": total_count,
"hasMore": (offset + limit) < total_count
}
# Set up MQTT event logging # Set up MQTT event logging
async def log_mqtt_event(action: str, success: bool = True): async def log_mqtt_event(action: str, success: bool = True):
"""Log MQTT events to the database and log file""" """Log MQTT events to the database and log file"""
logger.info(f"MQTT Event - {action} (Success: {success})") logger.info(f"MQTT Event - {action} (Success: {success})")
async with get_db() as db: await add_event(datetime.utcnow().isoformat(), action, "MQTT", success)
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) ha_mqtt.set_event_callback(log_mqtt_event)
@ -144,7 +240,8 @@ app.add_middleware(
) )
# Gate control # Gate control
async def trigger_gate() -> bool: async def trigger_gate():
"""Trigger the gate relay"""
try: try:
settings = app.state.current_settings settings = app.state.current_settings
if not settings: if not settings:
@ -152,12 +249,29 @@ async def trigger_gate() -> bool:
settings = Settings() settings = Settings()
gate_pin = settings.gpio.gatePin gate_pin = settings.gpio.gatePin
trigger_duration = settings.triggerDuration / 1000.0 # Convert to seconds
# Activate relay (pull pin HIGH)
GPIO.output(gate_pin, GPIO.HIGH) GPIO.output(gate_pin, GPIO.HIGH)
await asyncio.sleep(settings.triggerDuration / 1000) # Convert ms to seconds logger.info(f"Gate triggered - pin {gate_pin} set HIGH")
# Log event
timestamp = datetime.now().isoformat()
await add_event(timestamp, "gate triggered", "api")
# Wait for specified duration
await asyncio.sleep(trigger_duration)
# Deactivate relay (pull pin LOW)
GPIO.output(gate_pin, GPIO.LOW) GPIO.output(gate_pin, GPIO.LOW)
logger.info(f"Gate trigger complete - pin {gate_pin} set LOW")
return True return True
except Exception as e: except Exception as e:
logger.error(f"Error triggering gate: {e}") logger.error(f"Failed to trigger gate: {e}", exc_info=True)
# Log failure
timestamp = datetime.now().isoformat()
await add_event(timestamp, "gate trigger failed", "api", False)
return False return False
last_open_time = None last_open_time = None
@ -169,59 +283,49 @@ async def update_gate_status():
global gate_monitor_running global gate_monitor_running
if gate_monitor_running: if gate_monitor_running:
logger.warning("Gate status monitor already running, skipping...") logger.warning("Gate status monitor already running")
return return
gate_monitor_running = True gate_monitor_running = True
logger.info("Starting gate status monitoring task") consecutive_errors = 0
try: try:
settings = app.state.current_settings
if not settings:
logger.warning("No settings available, using default settings")
settings = Settings()
status_pin = settings.gpio.statusPin
last_status = None
consecutive_errors = 0
while True: while True:
try: try:
if not gate_monitor_running: settings = app.state.current_settings
logger.info("Gate status monitor stopped") if not settings:
break logger.warning("No settings available, using default settings")
settings = Settings()
current_status = GPIO.input(status_pin) == GPIO.HIGH # Check current status (LOW = closed, HIGH = open)
is_open = GPIO.input(settings.gpio.statusPin) == GPIO.HIGH
if last_status != current_status: # Update state machine
timestamp = datetime.now() if gate_status.update(is_open):
logger.info(f"Gate status changed to: {'open' if current_status else 'closed'}") logger.info(f"Gate status changed to: {gate_status.state.name}")
logger.debug("Updating database with new status") logger.debug("Updating database with new status")
async with get_db() as db: # Update database
await db.execute( await add_gate_status(gate_status.last_changed)
"INSERT INTO gate_status (timestamp) VALUES (?)", await add_event(
(timestamp.isoformat(),) gate_status.last_changed,
) f"gate {gate_status.state.name.lower()}",
"sensor"
)
await db.execute( # Update MQTT state if enabled
"INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)", if settings.mqtt.enabled:
(timestamp.isoformat(), f"gate {'opened' if current_status else 'closed'}", "sensor", True) await publish_mqtt_state(gate_status.is_open)
)
await db.commit()
await ha_mqtt.publish_state(current_status)
last_status = current_status
consecutive_errors = 0 consecutive_errors = 0
else:
logger.debug(f"Gate status unchanged: {'open' if current_status else 'closed'}")
# Sleep for a short time before next check
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
except Exception as e: except Exception as e:
consecutive_errors += 1 consecutive_errors += 1
wait_time = min(30, 2 ** consecutive_errors) wait_time = min(30, 2 ** consecutive_errors)
logger.error(f"Error in update_gate_status (attempt {consecutive_errors}): {e}", exc_info=True) logger.error(f"Error in gate_status_monitor (attempt {consecutive_errors}): {e}", exc_info=True)
logger.warning(f"Retrying in {wait_time} seconds...") logger.warning(f"Retrying in {wait_time} seconds...")
await asyncio.sleep(wait_time) await asyncio.sleep(wait_time)
finally: finally:
@ -268,12 +372,7 @@ async def check_auto_close():
logger.warning(f"Gate has been open for {time_open:.1f} seconds, auto-closing") logger.warning(f"Gate has been open for {time_open:.1f} seconds, auto-closing")
timestamp = current_time.isoformat() timestamp = current_time.isoformat()
async with get_db() as db: await add_event(timestamp, "auto-close", "system")
await db.execute(
"INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)",
(timestamp, "auto-close", "system", True)
)
await db.commit()
await trigger_gate() await trigger_gate()
last_open_time = None last_open_time = None
@ -323,14 +422,6 @@ async def trigger():
settings = app.state.current_settings or Settings() settings = app.state.current_settings or Settings()
current_status = GPIO.input(settings.gpio.statusPin) == GPIO.HIGH current_status = GPIO.input(settings.gpio.statusPin) == GPIO.HIGH
# Log event
async with get_db() as db:
await db.execute(
"INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)",
(timestamp, "trigger gate", "api", success)
)
await db.commit()
return {"success": success, "timestamp": timestamp, "isOpen": current_status} return {"success": success, "timestamp": timestamp, "isOpen": current_status}
except Exception as e: except Exception as e:
logger.error("Error triggering gate", exc_info=True) logger.error("Error triggering gate", exc_info=True)
@ -341,16 +432,11 @@ async def get_status():
"""Get current gate status""" """Get current gate status"""
try: try:
settings = app.state.current_settings or Settings() settings = app.state.current_settings or Settings()
# LOW (0V) means gate is closed, HIGH (3.3V) means gate is open
is_open = GPIO.input(settings.gpio.statusPin) == GPIO.HIGH is_open = GPIO.input(settings.gpio.statusPin) == GPIO.HIGH
gate_status.update(is_open)
async with get_db() as db: return {"isOpen": gate_status.is_open, "lastChanged": gate_status.last_changed}
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()
return {"isOpen": is_open, "lastChanged": last_changed}
except Exception as e: except Exception as e:
logger.error("Error getting gate status", exc_info=True) logger.error("Error getting gate status", exc_info=True)
raise HTTPException(status_code=500, detail="Failed to get gate status") raise HTTPException(status_code=500, detail="Failed to get gate status")
@ -358,44 +444,17 @@ async def get_status():
@app.get("/api/events") @app.get("/api/events")
async def get_events(limit: int = 10, offset: int = 0): async def get_events(limit: int = 10, offset: int = 0):
"""Get recent gate events with pagination""" """Get recent gate events with pagination"""
async with get_db() as db: return await get_events(limit, offset)
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 ? OFFSET ?
""",
(limit, offset)
)
events = await cursor.fetchall()
return {
"events": [dict(event) for event in events],
"total": total_count,
"hasMore": (offset + limit) < total_count
}
@app.get("/api/settings") @app.get("/api/settings")
async def get_settings(): async def get_settings_route():
"""Get current settings""" """Get current settings"""
async with get_db() as db: try:
cursor = await db.execute("SELECT key, value FROM settings") settings_dict = await get_settings()
rows = await cursor.fetchall()
settings = {}
for key, value in rows:
settings[key] = json.loads(value)
return { return {
"maxOpenTimeSeconds": settings.get("maxOpenTimeSeconds", "300"), "maxOpenTimeSeconds": settings_dict.get("maxOpenTimeSeconds", 300),
"triggerDuration": settings.get("triggerDuration", "500"), "triggerDuration": settings_dict.get("triggerDuration", 500),
"mqtt": settings.get("mqtt", { "mqtt": settings_dict.get("mqtt", {
"broker": "localhost", "broker": "localhost",
"port": "1883", "port": "1883",
"username": "", "username": "",
@ -403,83 +462,81 @@ async def get_settings():
"clientId": "gatekeeper", "clientId": "gatekeeper",
"enabled": False "enabled": False
}), }),
"gpio": settings.get("gpio", { "gpio": settings_dict.get("gpio", {
"gatePin": 17, "gatePin": 15,
"statusPin": 27 "statusPin": 7
}), }),
"logging": settings.get("logging", { "logging": settings_dict.get("logging", {
"level": "WARNING", "level": "WARNING",
"maxBytes": 10 * 1024 * 1024, "maxBytes": 10 * 1024 * 1024,
"backupCount": 5 "backupCount": 5
}) })
} }
except Exception as e:
logger.error(f"Failed to get settings: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Failed to get settings")
@app.post("/api/settings") @app.post("/api/settings")
async def update_settings(settings: Settings): async def update_settings_route(settings_update: Dict[str, Any]):
"""Update settings""" """Update settings"""
try: try:
async with get_db() as db: # Store original settings for comparison
# Update each setting old_settings = await get_settings()
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 # Update settings in database
if settings.mqtt: await update_settings(settings_update)
# Update environment variables
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 # Load new settings and merge with existing
ha_mqtt.enable(settings.mqtt.get("enabled", False)) current_settings = await get_settings()
new_settings = Settings(**current_settings) # Use complete settings
app.state.current_settings = new_settings
# Log settings update event with details # Update MQTT if configuration changed
changes = [] if "mqtt" in settings_update:
if settings.maxOpenTimeSeconds: if new_settings.mqtt.enabled:
changes.append(f"Max Open Time: {settings.maxOpenTimeSeconds}s") os.environ["MQTT_BROKER"] = new_settings.mqtt.broker
if settings.triggerDuration: os.environ["MQTT_PORT"] = new_settings.mqtt.port
changes.append(f"Trigger Duration: {settings.triggerDuration}ms") os.environ["MQTT_USERNAME"] = new_settings.mqtt.username
if settings.mqtt: os.environ["MQTT_PASSWORD"] = new_settings.mqtt.password
mqtt_status = "Enabled" if settings.mqtt.get("enabled") else "Disabled" os.environ["MQTT_CLIENT_ID"] = new_settings.mqtt.clientId
changes.append(f"MQTT: {mqtt_status}") await init_mqtt(new_settings)
if settings.mqtt.get("enabled"):
changes.append(f"Broker: {settings.mqtt['broker']}:{settings.mqtt['port']}")
await db.execute( # Update logging if configuration changed
"INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)", if "logging" in settings_update:
( setup_logging(new_settings)
datetime.utcnow().isoformat(),
f"Settings Updated ({'; '.join(changes)})",
"Settings",
True
)
)
await db.commit()
# Update logging configuration # Log changes
setup_logging(settings) timestamp = datetime.now().isoformat()
changes = []
if "maxOpenTimeSeconds" in settings_update:
changes.append(f"Max Open Time: {new_settings.maxOpenTimeSeconds}s")
if "triggerDuration" in settings_update:
changes.append(f"Trigger Duration: {new_settings.triggerDuration}ms")
if "mqtt" in settings_update:
mqtt_status = "Enabled" if new_settings.mqtt.enabled else "Disabled"
changes.append(f"MQTT: {mqtt_status}")
if new_settings.mqtt.enabled:
changes.append(f"Broker: {new_settings.mqtt.broker}:{new_settings.mqtt.port}")
await add_event(
timestamp,
f"Settings Updated ({'; '.join(changes)})",
"api"
)
return {"success": True, "message": "Settings updated successfully"}
return {"success": True}
except Exception as e: except Exception as e:
# Log failure event logger.error(f"Failed to update settings: {e}", exc_info=True)
async with get_db() as db: # Log failure
await db.execute( timestamp = datetime.now().isoformat()
"INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)", await add_event(
( timestamp,
datetime.utcnow().isoformat(), f"Settings update failed: {str(e)}",
f"Settings Update Failed: {str(e)}", "api",
"Settings", False
False )
) raise HTTPException(status_code=500, detail=f"Failed to update settings: {str(e)}")
)
await db.commit()
raise HTTPException(status_code=500, detail=str(e))
# Serve static files # Serve static files
app.mount("/", StaticFiles(directory="../public", html=True), name="static") app.mount("/", StaticFiles(directory="../public", html=True), name="static")
@ -553,91 +610,173 @@ async def init_db():
# Load settings from database # Load settings from database
async def load_settings(): async def load_settings():
"""Load settings and initialize components based on settings""" """Load settings from database"""
try: try:
# Use existing get_settings function
settings_dict = await get_settings() settings_dict = await get_settings()
settings = Settings(**settings_dict) settings = Settings(**settings_dict)
# Store settings in app state for easy access # Validate settings
app.state.current_settings = settings is_valid, errors = validate_settings(settings)
if not is_valid:
logger.warning(f"Invalid settings detected: {', '.join(errors)}")
logger.info("Using default settings")
settings = Settings() # Use defaults
# Configure MQTT based on settings # Store in app state
if settings.mqtt: app.state.current_settings = settings
# Update environment variables return settings
except Exception as e:
logger.error(f"Failed to load settings: {e}", exc_info=True)
# Return default settings on error
default_settings = Settings()
app.state.current_settings = default_settings
return default_settings
def validate_settings(settings: Settings) -> tuple[bool, list[str]]:
"""Validate settings and return (is_valid, error_messages)"""
errors = []
# Validate time settings
if settings.maxOpenTimeSeconds < 0:
errors.append("maxOpenTimeSeconds must be positive")
if settings.triggerDuration < 100: # Min 100ms
errors.append("triggerDuration must be at least 100ms")
# Validate GPIO settings
if settings.gpio.gatePin < 0 or settings.gpio.statusPin < 0:
errors.append("GPIO pins must be positive")
if settings.gpio.gatePin == settings.gpio.statusPin:
errors.append("Gate and status pins must be different")
# Validate MQTT settings if enabled
if settings.mqtt.enabled:
if not settings.mqtt.broker:
errors.append("MQTT broker required when MQTT is enabled")
if not settings.mqtt.port:
errors.append("MQTT port required when MQTT is enabled")
return len(errors) == 0, errors
async def init_mqtt(settings: Settings):
"""Initialize MQTT if enabled in settings"""
if settings.mqtt.enabled:
try:
logger.info("MQTT enabled, initializing connection...")
os.environ["MQTT_BROKER"] = settings.mqtt.broker os.environ["MQTT_BROKER"] = settings.mqtt.broker
os.environ["MQTT_PORT"] = settings.mqtt.port os.environ["MQTT_PORT"] = settings.mqtt.port
os.environ["MQTT_USERNAME"] = settings.mqtt.username os.environ["MQTT_USERNAME"] = settings.mqtt.username
os.environ["MQTT_PASSWORD"] = settings.mqtt.password os.environ["MQTT_PASSWORD"] = settings.mqtt.password
os.environ["MQTT_CLIENT_ID"] = settings.mqtt.clientId os.environ["MQTT_CLIENT_ID"] = settings.mqtt.clientId
# Enable/disable MQTT based on settings # Try to enable MQTT with timeout
if settings.mqtt.enabled: try:
logger.info("MQTT enabled in settings, initializing connection") async with asyncio.timeout(10): # 10 second timeout
ha_mqtt.enable() ha_mqtt.enable()
else: logger.info("MQTT initialized successfully")
logger.info("MQTT disabled in settings") except asyncio.TimeoutError:
logger.error("MQTT initialization timed out")
return False
# Set other application settings except Exception as e:
os.environ["MAX_OPEN_TIME_SECONDS"] = str(settings.maxOpenTimeSeconds) logger.error(f"Failed to initialize MQTT: {e}", exc_info=True)
os.environ["TRIGGER_DURATION"] = str(settings.triggerDuration) return False
else:
logger.info("MQTT disabled in settings")
return True
# Configure logging async def publish_mqtt_state(state: bool) -> bool:
setup_logging(settings) """Publish state to MQTT with error handling"""
if not app.state.current_settings.mqtt.enabled:
return True
logger.info(f"Settings loaded successfully (Max Open Time: {settings.maxOpenTimeSeconds}s, " try:
f"Trigger Duration: {settings.triggerDuration}ms, " async with asyncio.timeout(5): # 5 second timeout
f"Gate Pin: {settings.gpio.gatePin}, Status Pin: {settings.gpio.statusPin})") await ha_mqtt.publish_state(state)
return settings return True
except (asyncio.TimeoutError, Exception) as e:
logger.error(f"Failed to publish MQTT state: {e}", exc_info=True)
return False
async def start_background_tasks():
"""Start all background monitoring tasks"""
logger.info("Starting background tasks...")
global gate_monitor_running, auto_close_running
gate_monitor_running = True
auto_close_running = True
app.state.status_task = asyncio.create_task(update_gate_status())
app.state.auto_close_task = asyncio.create_task(check_auto_close())
logger.info("Background tasks started")
async def validate_startup_state() -> tuple[bool, list[str]]:
"""Validate application startup state"""
errors = []
# Check database
try:
async with get_db() as db:
await db.execute("SELECT 1")
except Exception as e: except Exception as e:
logger.warning(f"Failed to load settings: {e}", exc_info=True) errors.append(f"Database not accessible: {e}")
logger.info("Using default settings")
default_settings = Settings() # Check GPIO
app.state.current_settings = default_settings try:
return default_settings settings = app.state.current_settings
if not settings:
errors.append("Settings not loaded")
else:
# Check if pins are configured
GPIO.input(settings.gpio.statusPin) # Test read
GPIO.output(settings.gpio.gatePin, GPIO.LOW) # Test write
except Exception as e:
errors.append(f"GPIO not properly configured: {e}")
# Check MQTT if enabled
if app.state.current_settings and app.state.current_settings.mqtt.enabled:
if not ha_mqtt.is_connected():
errors.append("MQTT enabled but not connected")
return len(errors) == 0, errors
@app.on_event("startup") @app.on_event("startup")
async def startup_event(): async def startup_event():
"""Initialize the application on startup""" """Initialize the application on startup"""
try: try:
logger.info("Starting Gatekeeper application") logger.info("Starting application...")
# 1. Initialize database # 1. Initialize database
logger.info("Initializing database...") logger.info("Initializing database...")
await init_db() await init_db()
logger.info("Database initialized successfully")
# 2. Load settings from database # 2. Load settings
logger.info("Loading settings...") logger.info("Loading settings...")
settings = await load_settings() settings = await load_settings()
app.state.current_settings = settings
logger.info("Settings loaded successfully")
# 3. Setup GPIO # 3. Configure logging based on settings
logger.info("Initializing GPIO...") logger.info("Configuring logging...")
setup_logging(settings)
# 4. Configure MQTT
await init_mqtt(settings)
# 5. Initialize GPIO
logger.info("Setting up GPIO...")
setup_gpio() setup_gpio()
logger.info("GPIO initialized successfully")
# 4. Initialize MQTT if enabled # 6. Start background tasks
if settings.mqtt.enabled: await start_background_tasks()
logger.info("MQTT enabled, initializing connection...")
ha_mqtt.enable() # 7. Validate startup state
logger.info("MQTT initialized successfully") is_valid, errors = await validate_startup_state()
if not is_valid:
logger.warning("Application started with warnings:")
for error in errors:
logger.warning(f" - {error}")
else: else:
logger.info("MQTT disabled in settings") logger.info("Application startup complete and validated")
# 5. Start background tasks
logger.info("Starting background tasks...")
global gate_monitor_running, auto_close_running
gate_monitor_running = True
auto_close_running = True
app.state.status_task = asyncio.create_task(update_gate_status())
app.state.auto_close_task = asyncio.create_task(check_auto_close())
logger.info("Background tasks started successfully")
logger.info("Gatekeeper application started successfully")
except Exception as e: except Exception as e:
logger.critical("Failed to start application", exc_info=True) logger.error(f"Failed to start application: {e}", exc_info=True)
raise raise
@app.on_event("shutdown") @app.on_event("shutdown")