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:
parent
a3ecf3a606
commit
9046cbca1d
567
backend/main.py
567
backend/main.py
|
|
@ -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")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue