fix: align frontend-backend types and API responses
- Updated Settings interface to use numbers for maxOpenTimeSeconds and triggerDuration - Fixed triggerGate response type to match backend - Updated SettingsDialog to handle numeric inputs correctly
This commit is contained in:
parent
c4305643f4
commit
0277e3d6ec
620
backend/main.py
620
backend/main.py
|
|
@ -11,7 +11,7 @@ from fastapi import FastAPI, HTTPException
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Optional, Dict, Any, List
|
from typing import Optional, Dict, Any, List, Union
|
||||||
import RPi.GPIO as GPIO
|
import RPi.GPIO as GPIO
|
||||||
|
|
||||||
from mqtt_integration import HomeAssistantMQTT
|
from mqtt_integration import HomeAssistantMQTT
|
||||||
|
|
@ -19,26 +19,72 @@ from mqtt_integration import HomeAssistantMQTT
|
||||||
# Configure logging
|
# Configure logging
|
||||||
LOG_FILE = "/var/log/gatekeeper.log"
|
LOG_FILE = "/var/log/gatekeeper.log"
|
||||||
logger = logging.getLogger("gatekeeper")
|
logger = logging.getLogger("gatekeeper")
|
||||||
logger.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
# Create rotating file handler (10MB per file, keep 5 backup files)
|
# Models
|
||||||
file_handler = RotatingFileHandler(
|
class GateEvent(BaseModel):
|
||||||
|
id: Optional[int] = None
|
||||||
|
timestamp: str
|
||||||
|
action: str
|
||||||
|
source: str
|
||||||
|
success: bool
|
||||||
|
|
||||||
|
class MQTTSettings(BaseModel):
|
||||||
|
broker: str = "localhost"
|
||||||
|
port: str = "1883"
|
||||||
|
username: str = ""
|
||||||
|
password: str = ""
|
||||||
|
clientId: str = "gatekeeper"
|
||||||
|
enabled: bool = False
|
||||||
|
|
||||||
|
class GPIOSettings(BaseModel):
|
||||||
|
gatePin: int = 17 # Default GPIO pin for gate control
|
||||||
|
statusPin: int = 27 # Default GPIO pin for gate status
|
||||||
|
|
||||||
|
class LoggingSettings(BaseModel):
|
||||||
|
level: str = "WARNING" # Default to WARNING level
|
||||||
|
maxBytes: int = 10 * 1024 * 1024 # 10MB default
|
||||||
|
backupCount: int = 5 # Keep 5 backup files by default
|
||||||
|
|
||||||
|
class Settings(BaseModel):
|
||||||
|
maxOpenTimeSeconds: int = 300 # Default 5 minutes
|
||||||
|
triggerDuration: int = 500 # Default 500ms
|
||||||
|
mqtt: MQTTSettings = MQTTSettings()
|
||||||
|
gpio: GPIOSettings = GPIOSettings()
|
||||||
|
logging: LoggingSettings = LoggingSettings()
|
||||||
|
|
||||||
|
class GateStatus(BaseModel):
|
||||||
|
isOpen: bool
|
||||||
|
lastChanged: str
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
def setup_logging(settings: Settings):
|
||||||
|
"""Configure logging based on settings"""
|
||||||
|
log_level = getattr(logging, settings.logging.level.upper(), logging.WARNING)
|
||||||
|
logger.setLevel(log_level)
|
||||||
|
|
||||||
|
# Remove existing handlers
|
||||||
|
for handler in logger.handlers[:]:
|
||||||
|
logger.removeHandler(handler)
|
||||||
|
|
||||||
|
# Create rotating file handler
|
||||||
|
file_handler = RotatingFileHandler(
|
||||||
LOG_FILE,
|
LOG_FILE,
|
||||||
maxBytes=10*1024*1024, # 10MB
|
maxBytes=settings.logging.maxBytes,
|
||||||
backupCount=5,
|
backupCount=settings.logging.backupCount,
|
||||||
delay=True # Only create log file when first record is written
|
delay=True # Only create log file when first record is written
|
||||||
)
|
)
|
||||||
file_handler.setLevel(logging.INFO)
|
file_handler.setLevel(log_level)
|
||||||
|
|
||||||
# Create formatter and add it to the handler
|
# Create formatter and add it to the handler
|
||||||
formatter = logging.Formatter(
|
formatter = logging.Formatter(
|
||||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
datefmt='%Y-%m-%d %H:%M:%S'
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
)
|
)
|
||||||
file_handler.setFormatter(formatter)
|
file_handler.setFormatter(formatter)
|
||||||
|
|
||||||
# Add the handler to the logger
|
# Add the handler to the logger
|
||||||
logger.addHandler(file_handler)
|
logger.addHandler(file_handler)
|
||||||
|
logger.debug("Logging configured with level: %s", settings.logging.level)
|
||||||
|
|
||||||
# Log uncaught exceptions
|
# Log uncaught exceptions
|
||||||
def handle_exception(exc_type, exc_value, exc_traceback):
|
def handle_exception(exc_type, exc_value, exc_traceback):
|
||||||
|
|
@ -55,11 +101,14 @@ sys.excepthook = handle_exception
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
ha_mqtt = HomeAssistantMQTT()
|
ha_mqtt = HomeAssistantMQTT()
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
DB_PATH = "gate.db" # Database file path
|
||||||
|
|
||||||
# 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 aiosqlite.connect("gate.db") as db:
|
async with await get_db() as db:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)",
|
"INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)",
|
||||||
(datetime.utcnow().isoformat(), action, "MQTT", success)
|
(datetime.utcnow().isoformat(), action, "MQTT", success)
|
||||||
|
|
@ -77,203 +126,170 @@ app.add_middleware(
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Constants
|
|
||||||
DB_PATH = "gate.db"
|
|
||||||
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):
|
|
||||||
id: Optional[int] = None
|
|
||||||
timestamp: str
|
|
||||||
action: str
|
|
||||||
source: str
|
|
||||||
success: bool
|
|
||||||
|
|
||||||
class Settings(BaseModel):
|
|
||||||
maxOpenTimeSeconds: str
|
|
||||||
triggerDuration: str
|
|
||||||
mqtt: dict
|
|
||||||
|
|
||||||
class GateStatus(BaseModel):
|
|
||||||
isOpen: bool
|
|
||||||
lastChanged: str
|
|
||||||
|
|
||||||
# GPIO Setup
|
|
||||||
def setup_gpio():
|
|
||||||
GPIO.setwarnings(False)
|
|
||||||
GPIO.setmode(GPIO.BCM)
|
|
||||||
|
|
||||||
# Setup relays as outputs (LOW is off)
|
|
||||||
GPIO.setup(RELAY_1_PIN, GPIO.OUT)
|
|
||||||
GPIO.setup(RELAY_2_PIN, GPIO.OUT)
|
|
||||||
GPIO.output(RELAY_1_PIN, GPIO.LOW)
|
|
||||||
GPIO.output(RELAY_2_PIN, GPIO.LOW)
|
|
||||||
|
|
||||||
# Setup status pin as input with pull-down
|
|
||||||
# This means it will read LOW when floating
|
|
||||||
GPIO.setup(STATUS_PIN, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
|
|
||||||
|
|
||||||
logger.info(f"GPIO initialized: Relay 1 on GPIO{RELAY_1_PIN}, Relay 2 on GPIO{RELAY_2_PIN}, Status on GPIO{STATUS_PIN}")
|
|
||||||
|
|
||||||
# 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,
|
|
||||||
timestamp TEXT NOT NULL,
|
|
||||||
action TEXT NOT NULL,
|
|
||||||
source TEXT NOT NULL,
|
|
||||||
success BOOLEAN 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
|
|
||||||
)
|
|
||||||
""")
|
|
||||||
|
|
||||||
# Create settings table
|
|
||||||
await db.execute("""
|
|
||||||
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
|
# Gate control
|
||||||
async def trigger_gate() -> bool:
|
async def trigger_gate() -> bool:
|
||||||
try:
|
try:
|
||||||
GPIO.output(RELAY_1_PIN, GPIO.HIGH)
|
settings = app.state.current_settings
|
||||||
await asyncio.sleep(TRIGGER_DURATION / 1000) # Convert to seconds
|
if not settings:
|
||||||
GPIO.output(RELAY_1_PIN, GPIO.LOW)
|
logger.warning("No settings available, using default settings")
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
gate_pin = settings.gpio.gatePin
|
||||||
|
GPIO.output(gate_pin, GPIO.HIGH)
|
||||||
|
await asyncio.sleep(settings.triggerDuration / 1000) # Convert ms to seconds
|
||||||
|
GPIO.output(gate_pin, GPIO.LOW)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error triggering gate: {e}")
|
logger.error(f"Error triggering gate: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
last_open_time = None
|
last_open_time = None
|
||||||
|
gate_monitor_running = False
|
||||||
async def check_auto_close():
|
auto_close_running = False
|
||||||
"""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():
|
async def update_gate_status():
|
||||||
"""Monitor gate status and update database when it changes"""
|
"""Monitor gate status and update database when it changes"""
|
||||||
global last_open_time
|
global gate_monitor_running
|
||||||
|
|
||||||
|
if gate_monitor_running:
|
||||||
|
logger.warning("Gate status monitor already running, skipping...")
|
||||||
|
return
|
||||||
|
|
||||||
|
gate_monitor_running = True
|
||||||
|
logger.info("Starting gate status monitoring task")
|
||||||
|
|
||||||
|
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
|
last_status = None
|
||||||
|
consecutive_errors = 0
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
current_status = GPIO.input(STATUS_PIN) == GPIO.HIGH # True = OPEN, False = CLOSED
|
if not gate_monitor_running:
|
||||||
|
logger.info("Gate status monitor stopped")
|
||||||
|
break
|
||||||
|
|
||||||
|
current_status = GPIO.input(status_pin) == GPIO.HIGH
|
||||||
|
|
||||||
if last_status != current_status:
|
if last_status != current_status:
|
||||||
timestamp = datetime.now()
|
timestamp = datetime.now()
|
||||||
|
logger.info(f"Gate status changed to: {'open' if current_status else 'closed'}")
|
||||||
|
logger.debug("Updating database with new status")
|
||||||
|
|
||||||
# Update last_open_time when gate opens
|
async with await get_db() as db:
|
||||||
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(
|
await db.execute(
|
||||||
"INSERT INTO gate_status (timestamp) VALUES (?)",
|
"INSERT INTO gate_status (timestamp) VALUES (?)",
|
||||||
(timestamp.isoformat(),)
|
(timestamp.isoformat(),)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Log the status change as an event
|
|
||||||
status_str = "opened" if current_status else "closed"
|
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)",
|
"INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)",
|
||||||
(timestamp.isoformat(), f"gate {status_str}", "sensor", True)
|
(timestamp.isoformat(), f"gate {'opened' if current_status else 'closed'}", "sensor", True)
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
# Update Home Assistant via MQTT
|
|
||||||
await ha_mqtt.publish_state(current_status)
|
await ha_mqtt.publish_state(current_status)
|
||||||
last_status = current_status
|
last_status = current_status
|
||||||
logger.info(f"Gate status changed to: {'open' if current_status else 'closed'}")
|
consecutive_errors = 0
|
||||||
|
else:
|
||||||
|
logger.debug(f"Gate status unchanged: {'open' if current_status else 'closed'}")
|
||||||
|
|
||||||
await asyncio.sleep(0.5) # Check every 500ms
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in update_gate_status: {e}")
|
consecutive_errors += 1
|
||||||
await asyncio.sleep(5) # Wait longer on error
|
wait_time = min(30, 2 ** consecutive_errors)
|
||||||
|
logger.error(f"Error in update_gate_status (attempt {consecutive_errors}): {e}", exc_info=True)
|
||||||
|
logger.warning(f"Retrying in {wait_time} seconds...")
|
||||||
|
await asyncio.sleep(wait_time)
|
||||||
|
finally:
|
||||||
|
gate_monitor_running = False
|
||||||
|
logger.info("Gate status monitor stopped")
|
||||||
|
|
||||||
|
async def check_auto_close():
|
||||||
|
"""Check if gate has been open too long and close it if needed"""
|
||||||
|
global last_open_time, auto_close_running
|
||||||
|
|
||||||
|
if auto_close_running:
|
||||||
|
logger.warning("Auto-close monitor already running, skipping...")
|
||||||
|
return
|
||||||
|
|
||||||
|
auto_close_running = True
|
||||||
|
logger.info("Starting auto-close monitoring task")
|
||||||
|
|
||||||
|
try:
|
||||||
|
settings = app.state.current_settings
|
||||||
|
if not settings:
|
||||||
|
logger.warning("No settings available, using default settings")
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
status_pin = settings.gpio.statusPin
|
||||||
|
consecutive_errors = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
if not auto_close_running:
|
||||||
|
logger.info("Auto-close monitor stopped")
|
||||||
|
break
|
||||||
|
|
||||||
|
if GPIO.input(status_pin) == GPIO.HIGH: # Gate is open
|
||||||
|
current_time = datetime.now()
|
||||||
|
|
||||||
|
if last_open_time is None:
|
||||||
|
last_open_time = current_time
|
||||||
|
logger.debug("Gate opened, starting timer")
|
||||||
|
|
||||||
|
time_open = (current_time - last_open_time).total_seconds()
|
||||||
|
logger.debug(f"Gate has been open for {time_open:.1f} seconds")
|
||||||
|
|
||||||
|
if time_open > settings.maxOpenTimeSeconds:
|
||||||
|
logger.warning(f"Gate has been open for {time_open:.1f} seconds, auto-closing")
|
||||||
|
timestamp = current_time.isoformat()
|
||||||
|
|
||||||
|
async with await get_db() as db:
|
||||||
|
await db.execute(
|
||||||
|
"INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)",
|
||||||
|
(timestamp, "auto-close", "system", True)
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
await trigger_gate()
|
||||||
|
last_open_time = None
|
||||||
|
logger.info("Gate auto-closed successfully")
|
||||||
|
else:
|
||||||
|
if last_open_time is not None:
|
||||||
|
logger.debug("Gate is now closed, resetting timer")
|
||||||
|
last_open_time = None
|
||||||
|
|
||||||
|
consecutive_errors = 0
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
consecutive_errors += 1
|
||||||
|
wait_time = min(30, 2 ** consecutive_errors)
|
||||||
|
logger.error(f"Error in check_auto_close (attempt {consecutive_errors}): {e}", exc_info=True)
|
||||||
|
logger.warning(f"Retrying in {wait_time} seconds...")
|
||||||
|
await asyncio.sleep(wait_time)
|
||||||
|
finally:
|
||||||
|
auto_close_running = False
|
||||||
|
logger.info("Auto-close monitor stopped")
|
||||||
|
|
||||||
# MQQT Command Handler
|
# MQQT Command Handler
|
||||||
async def handle_mqtt_command(should_open: bool):
|
async def handle_mqtt_command(should_open: bool):
|
||||||
"""Handle commands received from Home Assistant"""
|
"""Handle commands received from Home Assistant"""
|
||||||
try:
|
try:
|
||||||
if should_open != (GPIO.input(STATUS_PIN) == GPIO.HIGH):
|
settings = app.state.current_settings
|
||||||
|
if not settings:
|
||||||
|
logger.warning("No settings available, using default settings")
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
status_pin = settings.gpio.statusPin
|
||||||
|
if should_open != (GPIO.input(status_pin) == GPIO.HIGH):
|
||||||
await trigger_gate()
|
await trigger_gate()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error handling MQTT command: {e}")
|
logger.error(f"Error handling MQTT command: {e}")
|
||||||
|
|
@ -281,42 +297,51 @@ async def handle_mqtt_command(should_open: bool):
|
||||||
# API Routes
|
# API Routes
|
||||||
@app.post("/api/trigger")
|
@app.post("/api/trigger")
|
||||||
async def trigger():
|
async def trigger():
|
||||||
|
"""Trigger the gate"""
|
||||||
|
try:
|
||||||
success = await trigger_gate()
|
success = await trigger_gate()
|
||||||
timestamp = datetime.now().isoformat()
|
timestamp = datetime.now().isoformat()
|
||||||
current_status = GPIO.input(STATUS_PIN) == GPIO.HIGH
|
|
||||||
action = "trigger gate"
|
|
||||||
|
|
||||||
async with aiosqlite.connect(DB_PATH) as db:
|
# Get current status after trigger
|
||||||
|
settings = app.state.current_settings or Settings()
|
||||||
|
current_status = GPIO.input(settings.gpio.statusPin) == GPIO.HIGH
|
||||||
|
|
||||||
|
# Log event
|
||||||
|
async with await get_db() as db:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)",
|
"INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)",
|
||||||
(timestamp, action, "api", success)
|
(timestamp, "trigger gate", "api", success)
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
return {"success": success, "currentStatus": current_status}
|
return {"success": success, "timestamp": timestamp, "isOpen": current_status}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Error triggering gate", exc_info=True)
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to trigger gate")
|
||||||
|
|
||||||
@app.get("/api/status")
|
@app.get("/api/status")
|
||||||
async def get_status():
|
async def get_status():
|
||||||
"""Get current gate status"""
|
"""Get current gate status"""
|
||||||
is_open = GPIO.input(STATUS_PIN) == GPIO.HIGH
|
try:
|
||||||
|
settings = app.state.current_settings or Settings()
|
||||||
|
is_open = GPIO.input(settings.gpio.statusPin) == GPIO.HIGH
|
||||||
|
|
||||||
# Get last status change time
|
async with await get_db() as db:
|
||||||
async with aiosqlite.connect(DB_PATH) as db:
|
|
||||||
cursor = await db.execute(
|
cursor = await db.execute(
|
||||||
"SELECT timestamp FROM gate_status ORDER BY timestamp DESC LIMIT 1"
|
"SELECT timestamp FROM gate_status ORDER BY timestamp DESC LIMIT 1"
|
||||||
)
|
)
|
||||||
result = await cursor.fetchone()
|
row = await cursor.fetchone()
|
||||||
last_changed = result[0] if result else datetime.now().isoformat()
|
last_changed = row[0] if row else datetime.now().isoformat()
|
||||||
|
|
||||||
return {
|
return {"isOpen": is_open, "lastChanged": last_changed}
|
||||||
"isOpen": is_open,
|
except Exception as e:
|
||||||
"lastChanged": last_changed
|
logger.error("Error getting gate status", exc_info=True)
|
||||||
}
|
raise HTTPException(status_code=500, detail="Failed to get gate 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 aiosqlite.connect(DB_PATH) as db:
|
async with await get_db() as db:
|
||||||
db.row_factory = aiosqlite.Row
|
db.row_factory = aiosqlite.Row
|
||||||
|
|
||||||
# Get total count
|
# Get total count
|
||||||
|
|
@ -343,7 +368,7 @@ async def get_events(limit: int = 10, offset: int = 0):
|
||||||
@app.get("/api/settings")
|
@app.get("/api/settings")
|
||||||
async def get_settings():
|
async def get_settings():
|
||||||
"""Get current settings"""
|
"""Get current settings"""
|
||||||
async with aiosqlite.connect(DB_PATH) as db:
|
async with await get_db() as db:
|
||||||
cursor = await db.execute("SELECT key, value FROM settings")
|
cursor = await db.execute("SELECT key, value FROM settings")
|
||||||
rows = await cursor.fetchall()
|
rows = await cursor.fetchall()
|
||||||
settings = {}
|
settings = {}
|
||||||
|
|
@ -360,6 +385,15 @@ async def get_settings():
|
||||||
"password": "",
|
"password": "",
|
||||||
"clientId": "gatekeeper",
|
"clientId": "gatekeeper",
|
||||||
"enabled": False
|
"enabled": False
|
||||||
|
}),
|
||||||
|
"gpio": settings.get("gpio", {
|
||||||
|
"gatePin": 17,
|
||||||
|
"statusPin": 27
|
||||||
|
}),
|
||||||
|
"logging": settings.get("logging", {
|
||||||
|
"level": "WARNING",
|
||||||
|
"maxBytes": 10 * 1024 * 1024,
|
||||||
|
"backupCount": 5
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -367,7 +401,7 @@ async def get_settings():
|
||||||
async def update_settings(settings: Settings):
|
async def update_settings(settings: Settings):
|
||||||
"""Update settings"""
|
"""Update settings"""
|
||||||
try:
|
try:
|
||||||
async with aiosqlite.connect(DB_PATH) as db:
|
async with await get_db() as db:
|
||||||
# Update each setting
|
# Update each setting
|
||||||
for key, value in settings.dict().items():
|
for key, value in settings.dict().items():
|
||||||
await db.execute(
|
await db.execute(
|
||||||
|
|
@ -378,6 +412,7 @@ async def update_settings(settings: Settings):
|
||||||
|
|
||||||
# Update environment variables and MQTT connection
|
# Update environment variables and MQTT connection
|
||||||
if settings.mqtt:
|
if settings.mqtt:
|
||||||
|
# Update environment variables
|
||||||
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"]
|
||||||
|
|
@ -410,10 +445,13 @@ async def update_settings(settings: Settings):
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
# Update logging configuration
|
||||||
|
setup_logging(settings)
|
||||||
|
|
||||||
return {"success": True}
|
return {"success": True}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Log failure event
|
# Log failure event
|
||||||
async with aiosqlite.connect(DB_PATH) as db:
|
async with await get_db() as db:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)",
|
"INSERT INTO events (timestamp, action, source, success) VALUES (?, ?, ?, ?)",
|
||||||
(
|
(
|
||||||
|
|
@ -429,52 +467,210 @@ async def update_settings(settings: Settings):
|
||||||
# Serve static files
|
# Serve static files
|
||||||
app.mount("/", StaticFiles(directory="../public", html=True), name="static")
|
app.mount("/", StaticFiles(directory="../public", html=True), name="static")
|
||||||
|
|
||||||
# Background task for monitoring gate status
|
# Database connection pool
|
||||||
@app.on_event("startup")
|
db_pool = None
|
||||||
async def startup_event():
|
|
||||||
"""Initialize the application on startup"""
|
async def get_db():
|
||||||
logger.info("Starting Gatekeeper application")
|
"""Get a database connection from the pool"""
|
||||||
|
global db_pool
|
||||||
|
if db_pool is None:
|
||||||
|
db_pool = await aiosqlite.connect(DB_PATH)
|
||||||
|
db_pool.row_factory = aiosqlite.Row
|
||||||
|
return db_pool
|
||||||
|
|
||||||
|
# GPIO Setup
|
||||||
|
def setup_gpio():
|
||||||
|
"""Initialize GPIO pins based on settings"""
|
||||||
|
GPIO.setwarnings(False)
|
||||||
|
GPIO.setmode(GPIO.BCM)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Initialize database
|
settings = app.state.current_settings
|
||||||
await init_db()
|
if not settings:
|
||||||
logger.info("Database initialized successfully")
|
logger.warning("No settings available, using default settings")
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
# Setup GPIO
|
# Setup gate control pin
|
||||||
setup_gpio()
|
gate_pin = settings.gpio.gatePin
|
||||||
logger.info("GPIO initialized successfully")
|
GPIO.setup(gate_pin, GPIO.OUT)
|
||||||
|
GPIO.output(gate_pin, GPIO.LOW)
|
||||||
|
|
||||||
# Initialize MQTT from settings
|
# Setup status pin if needed
|
||||||
settings = await get_settings()
|
status_pin = settings.gpio.statusPin
|
||||||
if settings.mqtt and settings.mqtt.get("enabled"):
|
GPIO.setup(status_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
|
||||||
|
|
||||||
|
logger.info(f"GPIO initialized (Gate Pin: {gate_pin}, Status Pin: {status_pin})")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to setup GPIO: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Database functions
|
||||||
|
async def init_db():
|
||||||
|
"""Initialize the SQLite database"""
|
||||||
|
async with await get_db() as db:
|
||||||
|
# Create events table
|
||||||
|
await db.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS events (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
timestamp TEXT NOT NULL,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
source TEXT NOT NULL,
|
||||||
|
success BOOLEAN 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
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Create settings table
|
||||||
|
await db.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Insert default settings if they don't exist
|
||||||
|
default_settings = Settings().dict()
|
||||||
|
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()
|
||||||
|
|
||||||
|
# Load settings from database
|
||||||
|
async def load_settings():
|
||||||
|
"""Load settings and initialize components based on settings"""
|
||||||
|
try:
|
||||||
|
settings_dict = await get_settings()
|
||||||
|
settings = Settings(**settings_dict)
|
||||||
|
|
||||||
|
# Store settings in app state for easy access
|
||||||
|
app.state.current_settings = settings
|
||||||
|
|
||||||
|
# Configure MQTT based on settings
|
||||||
|
if settings.mqtt:
|
||||||
|
# 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
|
||||||
|
if settings.mqtt.enabled:
|
||||||
logger.info("MQTT enabled in settings, initializing connection")
|
logger.info("MQTT enabled in settings, initializing connection")
|
||||||
ha_mqtt.enable()
|
ha_mqtt.enable()
|
||||||
else:
|
else:
|
||||||
logger.info("MQTT disabled in settings")
|
logger.info("MQTT disabled in settings")
|
||||||
|
|
||||||
# Start background tasks
|
# Set other application settings
|
||||||
|
os.environ["MAX_OPEN_TIME_SECONDS"] = str(settings.maxOpenTimeSeconds)
|
||||||
|
os.environ["TRIGGER_DURATION"] = str(settings.triggerDuration)
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
setup_logging(settings)
|
||||||
|
|
||||||
|
logger.info(f"Settings loaded successfully (Max Open Time: {settings.maxOpenTimeSeconds}s, "
|
||||||
|
f"Trigger Duration: {settings.triggerDuration}ms, "
|
||||||
|
f"Gate Pin: {settings.gpio.gatePin}, Status Pin: {settings.gpio.statusPin})")
|
||||||
|
return settings
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to load settings: {e}", exc_info=True)
|
||||||
|
logger.info("Using default settings")
|
||||||
|
default_settings = Settings()
|
||||||
|
app.state.current_settings = default_settings
|
||||||
|
return default_settings
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup_event():
|
||||||
|
"""Initialize the application on startup"""
|
||||||
|
try:
|
||||||
|
logger.info("Starting Gatekeeper application")
|
||||||
|
|
||||||
|
# 1. Initialize database
|
||||||
|
logger.info("Initializing database...")
|
||||||
|
await init_db()
|
||||||
|
logger.info("Database initialized successfully")
|
||||||
|
|
||||||
|
# 2. Load settings from database
|
||||||
|
logger.info("Loading settings...")
|
||||||
|
settings = await load_settings()
|
||||||
|
app.state.current_settings = settings
|
||||||
|
logger.info("Settings loaded successfully")
|
||||||
|
|
||||||
|
# 3. Setup GPIO
|
||||||
|
logger.info("Initializing GPIO...")
|
||||||
|
setup_gpio()
|
||||||
|
logger.info("GPIO initialized successfully")
|
||||||
|
|
||||||
|
# 4. Initialize MQTT if enabled
|
||||||
|
if settings.mqtt.enabled:
|
||||||
|
logger.info("MQTT enabled, initializing connection...")
|
||||||
|
ha_mqtt.enable()
|
||||||
|
logger.info("MQTT initialized successfully")
|
||||||
|
else:
|
||||||
|
logger.info("MQTT disabled in settings")
|
||||||
|
|
||||||
|
# 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.status_task = asyncio.create_task(update_gate_status())
|
||||||
app.state.auto_close_task = asyncio.create_task(check_auto_close())
|
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.error(f"Startup error: {str(e)}", exc_info=True)
|
logger.critical("Failed to start application", exc_info=True)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@app.on_event("shutdown")
|
@app.on_event("shutdown")
|
||||||
async def shutdown_event():
|
async def shutdown_event():
|
||||||
"""Cleanup on shutdown"""
|
"""Cleanup on shutdown"""
|
||||||
logger.info("Shutting down Gatekeeper application")
|
|
||||||
try:
|
try:
|
||||||
# Cancel background tasks
|
logger.info("Shutting down Gatekeeper application")
|
||||||
|
|
||||||
|
# 1. Stop background tasks
|
||||||
|
logger.info("Stopping background tasks...")
|
||||||
|
global gate_monitor_running, auto_close_running
|
||||||
|
gate_monitor_running = False
|
||||||
|
auto_close_running = False
|
||||||
|
|
||||||
if hasattr(app.state, "status_task"):
|
if hasattr(app.state, "status_task"):
|
||||||
app.state.status_task.cancel()
|
app.state.status_task.cancel()
|
||||||
if hasattr(app.state, "auto_close_task"):
|
if hasattr(app.state, "auto_close_task"):
|
||||||
app.state.auto_close_task.cancel()
|
app.state.auto_close_task.cancel()
|
||||||
|
logger.info("Background tasks stopped")
|
||||||
|
|
||||||
# Disconnect MQTT
|
# 2. Disconnect MQTT if it was enabled
|
||||||
|
if hasattr(app.state, "current_settings") and app.state.current_settings.mqtt.enabled:
|
||||||
|
logger.info("Disconnecting MQTT...")
|
||||||
await ha_mqtt.disconnect()
|
await ha_mqtt.disconnect()
|
||||||
|
logger.info("MQTT disconnected")
|
||||||
|
|
||||||
# Cleanup GPIO
|
# 3. Clean up GPIO
|
||||||
|
logger.info("Cleaning up GPIO...")
|
||||||
GPIO.cleanup()
|
GPIO.cleanup()
|
||||||
logger.info("Cleanup completed successfully")
|
logger.info("GPIO cleanup completed")
|
||||||
|
|
||||||
|
# 4. Close database connection
|
||||||
|
logger.info("Closing database connection...")
|
||||||
|
global db_pool
|
||||||
|
if db_pool:
|
||||||
|
await db_pool.close()
|
||||||
|
logger.info("Database connection closed")
|
||||||
|
|
||||||
|
logger.info("Shutdown completed successfully")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Shutdown error: {str(e)}", exc_info=True)
|
logger.error("Error during shutdown", exc_info=True)
|
||||||
raise
|
raise
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
"name": "dlb-gatekeeper-frontend",
|
"name": "dlb-gatekeeper-frontend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@headlessui/react": "^2.2.0",
|
||||||
"@heroicons/react": "^2.0.18",
|
"@heroicons/react": "^2.0.18",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|
@ -715,6 +716,78 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
|
||||||
|
"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.6.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
|
||||||
|
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.6.0",
|
||||||
|
"@floating-ui/utils": "^0.2.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/react": {
|
||||||
|
"version": "0.26.28",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
|
||||||
|
"integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react-dom": "^2.1.2",
|
||||||
|
"@floating-ui/utils": "^0.2.8",
|
||||||
|
"tabbable": "^6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/react-dom": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
||||||
|
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@headlessui/react": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react": "^0.26.16",
|
||||||
|
"@react-aria/focus": "^3.17.1",
|
||||||
|
"@react-aria/interactions": "^3.21.3",
|
||||||
|
"@tanstack/react-virtual": "^3.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@heroicons/react": {
|
"node_modules/@heroicons/react": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
|
||||||
|
|
@ -844,6 +917,89 @@
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-aria/focus": {
|
||||||
|
"version": "3.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.19.0.tgz",
|
||||||
|
"integrity": "sha512-hPF9EXoUQeQl1Y21/rbV2H4FdUR2v+4/I0/vB+8U3bT1CJ+1AFj1hc/rqx2DqEwDlEwOHN+E4+mRahQmlybq0A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-aria/interactions": "^3.22.5",
|
||||||
|
"@react-aria/utils": "^3.26.0",
|
||||||
|
"@react-types/shared": "^3.26.0",
|
||||||
|
"@swc/helpers": "^0.5.0",
|
||||||
|
"clsx": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-aria/interactions": {
|
||||||
|
"version": "3.22.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.5.tgz",
|
||||||
|
"integrity": "sha512-kMwiAD9E0TQp+XNnOs13yVJghiy8ET8L0cbkeuTgNI96sOAp/63EJ1FSrDf17iD8sdjt41LafwX/dKXW9nCcLQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-aria/ssr": "^3.9.7",
|
||||||
|
"@react-aria/utils": "^3.26.0",
|
||||||
|
"@react-types/shared": "^3.26.0",
|
||||||
|
"@swc/helpers": "^0.5.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-aria/ssr": {
|
||||||
|
"version": "3.9.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz",
|
||||||
|
"integrity": "sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@swc/helpers": "^0.5.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-aria/utils": {
|
||||||
|
"version": "3.26.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.26.0.tgz",
|
||||||
|
"integrity": "sha512-LkZouGSjjQ0rEqo4XJosS4L3YC/zzQkfRM3KoqK6fUOmUJ9t0jQ09WjiF+uOoG9u+p30AVg3TrZRUWmoTS+koQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-aria/ssr": "^3.9.7",
|
||||||
|
"@react-stately/utils": "^3.10.5",
|
||||||
|
"@react-types/shared": "^3.26.0",
|
||||||
|
"@swc/helpers": "^0.5.0",
|
||||||
|
"clsx": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-stately/utils": {
|
||||||
|
"version": "3.10.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.5.tgz",
|
||||||
|
"integrity": "sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@swc/helpers": "^0.5.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-types/shared": {
|
||||||
|
"version": "3.26.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.26.0.tgz",
|
||||||
|
"integrity": "sha512-6FuPqvhmjjlpEDLTiYx29IJCbCNWPlsyO+ZUmCUXzhUv2ttShOXfw8CmeHWHftT/b2KweAWuzqSlfeXPR76jpw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.29.2",
|
"version": "4.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.2.tgz",
|
||||||
|
|
@ -1110,6 +1266,42 @@
|
||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@swc/helpers": {
|
||||||
|
"version": "0.5.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||||
|
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-virtual": {
|
||||||
|
"version": "3.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz",
|
||||||
|
"integrity": "sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/virtual-core": "3.11.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/virtual-core": {
|
||||||
|
"version": "3.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz",
|
||||||
|
"integrity": "sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
|
|
@ -1474,6 +1666,15 @@
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/clsx": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
|
@ -2837,6 +3038,12 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tabbable": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.4.17",
|
"version": "3.4.17",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||||
|
|
@ -2918,6 +3125,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.7.2",
|
"version": "5.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@headlessui/react": "^2.2.0",
|
||||||
"@heroicons/react": "^2.0.18",
|
"@heroicons/react": "^2.0.18",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,15 @@ function App() {
|
||||||
password: '',
|
password: '',
|
||||||
clientId: 'gatekeeper',
|
clientId: 'gatekeeper',
|
||||||
enabled: false
|
enabled: false
|
||||||
|
},
|
||||||
|
gpio: {
|
||||||
|
gatePin: 17,
|
||||||
|
statusPin: 27
|
||||||
|
},
|
||||||
|
logging: {
|
||||||
|
level: 'WARNING',
|
||||||
|
maxBytes: 10 * 1024 * 1024, // 10MB
|
||||||
|
backupCount: 5
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const [gateStatus, setGateStatus] = useState<GateStatus>({ isOpen: false, lastChanged: new Date().toISOString() });
|
const [gateStatus, setGateStatus] = useState<GateStatus>({ isOpen: false, lastChanged: new Date().toISOString() });
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { GateEvent, Settings, GateStatus } from './types';
|
||||||
|
|
||||||
const API_BASE = process.env.NODE_ENV === 'production' ? '/api' : 'http://localhost:443/api';
|
const API_BASE = process.env.NODE_ENV === 'production' ? '/api' : 'http://localhost:443/api';
|
||||||
|
|
||||||
export async function triggerGate(): Promise<{ success: boolean, currentStatus: boolean }> {
|
export async function triggerGate(): Promise<{ success: boolean, timestamp: string, isOpen: boolean }> {
|
||||||
const response = await fetch(`${API_BASE}/trigger`, {
|
const response = await fetch(`${API_BASE}/trigger`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,29 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Settings, MQTTSettings } from '../types';
|
import { Settings, MQTTSettings } from '../types';
|
||||||
import { getSettings, updateSettings } from '../api';
|
import { getSettings, updateSettings } from '../api';
|
||||||
|
import { Tab } from '@headlessui/react';
|
||||||
|
import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||||
|
|
||||||
interface SettingsDialogProps {
|
interface SettingsDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function classNames(...classes: string[]) {
|
||||||
|
return classes.filter(Boolean).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
const TABS = [
|
||||||
|
{ name: 'Gate', icon: '🚪' },
|
||||||
|
{ name: 'GPIO', icon: '⚡' },
|
||||||
|
{ name: 'Logging', icon: '📝' },
|
||||||
|
{ name: 'MQTT', icon: '🔌' },
|
||||||
|
];
|
||||||
|
|
||||||
export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||||
const [settings, setSettings] = useState<Settings>({
|
const [settings, setSettings] = useState<Settings>({
|
||||||
maxOpenTimeSeconds: "300",
|
maxOpenTimeSeconds: 300,
|
||||||
triggerDuration: "500",
|
triggerDuration: 500,
|
||||||
mqtt: {
|
mqtt: {
|
||||||
broker: "localhost",
|
broker: "localhost",
|
||||||
port: "1883",
|
port: "1883",
|
||||||
|
|
@ -18,6 +31,15 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||||
password: "",
|
password: "",
|
||||||
clientId: "gatekeeper",
|
clientId: "gatekeeper",
|
||||||
enabled: false
|
enabled: false
|
||||||
|
},
|
||||||
|
gpio: {
|
||||||
|
gatePin: 17,
|
||||||
|
statusPin: 27
|
||||||
|
},
|
||||||
|
logging: {
|
||||||
|
level: "WARNING",
|
||||||
|
maxBytes: 10 * 1024 * 1024, // 10MB
|
||||||
|
backupCount: 5
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -61,11 +83,46 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/20 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
<div className="fixed inset-0 bg-black/20 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
||||||
<div className="bg-white/80 backdrop-blur-xl rounded-xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto border border-white/50">
|
<div className="bg-white/80 backdrop-blur-xl rounded-xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-hidden border border-white/50">
|
||||||
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
<form onSubmit={handleSubmit} className="h-full flex flex-col">
|
||||||
<h2 className="text-xl font-medium text-[#1d1d1f] mb-4">Settings</h2>
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||||
|
<h2 className="text-xl font-medium text-[#1d1d1f]">Settings</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-400 hover:text-gray-500"
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<Tab.Group>
|
||||||
|
<Tab.List className="flex space-x-1 border-b border-gray-200 px-6">
|
||||||
|
{TABS.map((tab) => (
|
||||||
|
<Tab
|
||||||
|
key={tab.name}
|
||||||
|
className={({ selected }) =>
|
||||||
|
classNames(
|
||||||
|
'py-3 px-4 text-sm font-medium border-b-2 outline-none',
|
||||||
|
selected
|
||||||
|
? 'border-[#0071e3] text-[#0071e3]'
|
||||||
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="flex items-center space-x-2">
|
||||||
|
<span>{tab.icon}</span>
|
||||||
|
<span>{tab.name}</span>
|
||||||
|
</span>
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
</Tab.List>
|
||||||
|
|
||||||
|
<Tab.Panels className="flex-1 overflow-y-auto">
|
||||||
{/* Gate Settings */}
|
{/* Gate Settings */}
|
||||||
|
<Tab.Panel className="p-6 space-y-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-base font-medium text-[#1d1d1f]">Gate Settings</h3>
|
<h3 className="text-base font-medium text-[#1d1d1f]">Gate Settings</h3>
|
||||||
<div className="grid grid-cols-1 gap-4">
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
|
@ -76,7 +133,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={settings.maxOpenTimeSeconds}
|
value={settings.maxOpenTimeSeconds}
|
||||||
onChange={(e) => setSettings(prev => ({ ...prev, maxOpenTimeSeconds: e.target.value }))}
|
onChange={(e) => setSettings(prev => ({ ...prev, maxOpenTimeSeconds: parseInt(e.target.value) || 0 }))}
|
||||||
className="mt-1 block w-full rounded-lg border border-[#e5e5e5] bg-white/50 backdrop-blur-sm
|
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
|
px-3 py-2 text-[#1d1d1f] shadow-sm
|
||||||
focus:outline-none focus:ring-2 focus:ring-[#0071e3]/30 focus:border-[#0071e3]
|
focus:outline-none focus:ring-2 focus:ring-[#0071e3]/30 focus:border-[#0071e3]
|
||||||
|
|
@ -90,7 +147,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={settings.triggerDuration}
|
value={settings.triggerDuration}
|
||||||
onChange={(e) => setSettings(prev => ({ ...prev, triggerDuration: e.target.value }))}
|
onChange={(e) => setSettings(prev => ({ ...prev, triggerDuration: parseInt(e.target.value) || 0 }))}
|
||||||
className="mt-1 block w-full rounded-lg border border-[#e5e5e5] bg-white/50 backdrop-blur-sm
|
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
|
px-3 py-2 text-[#1d1d1f] shadow-sm
|
||||||
focus:outline-none focus:ring-2 focus:ring-[#0071e3]/30 focus:border-[#0071e3]
|
focus:outline-none focus:ring-2 focus:ring-[#0071e3]/30 focus:border-[#0071e3]
|
||||||
|
|
@ -99,26 +156,140 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Tab.Panel>
|
||||||
|
|
||||||
|
{/* GPIO Settings */}
|
||||||
|
<Tab.Panel className="p-6 space-y-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-base font-medium text-[#1d1d1f]">GPIO Settings</h3>
|
||||||
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[#86868b] mb-1">
|
||||||
|
Gate Control Pin
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={settings.gpio.gatePin}
|
||||||
|
onChange={(e) => setSettings(prev => ({
|
||||||
|
...prev,
|
||||||
|
gpio: { ...prev.gpio, gatePin: parseInt(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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[#86868b] mb-1">
|
||||||
|
Gate Status Pin
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={settings.gpio.statusPin}
|
||||||
|
onChange={(e) => setSettings(prev => ({
|
||||||
|
...prev,
|
||||||
|
gpio: { ...prev.gpio, statusPin: parseInt(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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab.Panel>
|
||||||
|
|
||||||
|
{/* Logging Settings */}
|
||||||
|
<Tab.Panel className="p-6 space-y-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-base font-medium text-[#1d1d1f]">Logging Settings</h3>
|
||||||
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[#86868b] mb-1">
|
||||||
|
Log Level
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={settings.logging.level}
|
||||||
|
onChange={(e) => setSettings(prev => ({
|
||||||
|
...prev,
|
||||||
|
logging: { ...prev.logging, level: 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"
|
||||||
|
>
|
||||||
|
<option value="DEBUG">DEBUG</option>
|
||||||
|
<option value="INFO">INFO</option>
|
||||||
|
<option value="WARNING">WARNING</option>
|
||||||
|
<option value="ERROR">ERROR</option>
|
||||||
|
<option value="CRITICAL">CRITICAL</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[#86868b] mb-1">
|
||||||
|
Max Log File Size (bytes)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={settings.logging.maxBytes}
|
||||||
|
onChange={(e) => setSettings(prev => ({
|
||||||
|
...prev,
|
||||||
|
logging: { ...prev.logging, maxBytes: parseInt(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"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-[#86868b]">Default: 10MB (10485760 bytes)</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[#86868b] mb-1">
|
||||||
|
Number of Backup Files
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={settings.logging.backupCount}
|
||||||
|
onChange={(e) => setSettings(prev => ({
|
||||||
|
...prev,
|
||||||
|
logging: { ...prev.logging, backupCount: parseInt(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"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-[#86868b]">Number of old log files to keep</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab.Panel>
|
||||||
|
|
||||||
{/* MQTT Settings */}
|
{/* MQTT Settings */}
|
||||||
|
<Tab.Panel className="p-6 space-y-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-base font-medium text-[#1d1d1f]">MQTT Settings</h3>
|
<h3 className="text-base font-medium text-[#1d1d1f]">MQTT Settings</h3>
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
id="mqtt-enabled"
|
||||||
name="enabled"
|
name="enabled"
|
||||||
checked={settings.mqtt.enabled}
|
checked={settings.mqtt.enabled}
|
||||||
onChange={handleMQTTChange}
|
onChange={handleMQTTChange}
|
||||||
className="h-4 w-4 text-[#0071e3] focus:ring-[#0071e3]/30 border-[#e5e5e5] rounded
|
className="h-4 w-4 text-[#0071e3] border-gray-300 rounded
|
||||||
transition-colors duration-200"
|
focus:ring-[#0071e3] focus:ring-offset-0"
|
||||||
/>
|
/>
|
||||||
<label className="ml-2 block text-sm text-[#1d1d1f]">
|
<label htmlFor="mqtt-enabled" className="ml-2 text-sm text-gray-600">
|
||||||
Enable MQTT Integration
|
Enable MQTT
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[#86868b] mb-1">
|
<label className="block text-sm font-medium text-[#86868b] mb-1">
|
||||||
Broker Address
|
Broker Address
|
||||||
|
|
@ -130,9 +301,8 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||||
onChange={handleMQTTChange}
|
onChange={handleMQTTChange}
|
||||||
disabled={!settings.mqtt.enabled}
|
disabled={!settings.mqtt.enabled}
|
||||||
className="mt-1 block w-full rounded-lg border border-[#e5e5e5] bg-white/50 backdrop-blur-sm
|
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
|
px-3 py-2 text-[#1d1d1f] shadow-sm disabled:bg-gray-100 disabled:text-gray-500
|
||||||
focus:outline-none focus:ring-2 focus:ring-[#0071e3]/30 focus:border-[#0071e3]
|
focus:outline-none focus:ring-2 focus:ring-[#0071e3]/30 focus:border-[#0071e3]
|
||||||
disabled:bg-[#f5f5f7] disabled:text-[#86868b]
|
|
||||||
transition-colors duration-200"
|
transition-colors duration-200"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -147,9 +317,8 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||||
onChange={handleMQTTChange}
|
onChange={handleMQTTChange}
|
||||||
disabled={!settings.mqtt.enabled}
|
disabled={!settings.mqtt.enabled}
|
||||||
className="mt-1 block w-full rounded-lg border border-[#e5e5e5] bg-white/50 backdrop-blur-sm
|
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
|
px-3 py-2 text-[#1d1d1f] shadow-sm disabled:bg-gray-100 disabled:text-gray-500
|
||||||
focus:outline-none focus:ring-2 focus:ring-[#0071e3]/30 focus:border-[#0071e3]
|
focus:outline-none focus:ring-2 focus:ring-[#0071e3]/30 focus:border-[#0071e3]
|
||||||
disabled:bg-[#f5f5f7] disabled:text-[#86868b]
|
|
||||||
transition-colors duration-200"
|
transition-colors duration-200"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -164,9 +333,8 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||||
onChange={handleMQTTChange}
|
onChange={handleMQTTChange}
|
||||||
disabled={!settings.mqtt.enabled}
|
disabled={!settings.mqtt.enabled}
|
||||||
className="mt-1 block w-full rounded-lg border border-[#e5e5e5] bg-white/50 backdrop-blur-sm
|
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
|
px-3 py-2 text-[#1d1d1f] shadow-sm disabled:bg-gray-100 disabled:text-gray-500
|
||||||
focus:outline-none focus:ring-2 focus:ring-[#0071e3]/30 focus:border-[#0071e3]
|
focus:outline-none focus:ring-2 focus:ring-[#0071e3]/30 focus:border-[#0071e3]
|
||||||
disabled:bg-[#f5f5f7] disabled:text-[#86868b]
|
|
||||||
transition-colors duration-200"
|
transition-colors duration-200"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -181,9 +349,8 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||||
onChange={handleMQTTChange}
|
onChange={handleMQTTChange}
|
||||||
disabled={!settings.mqtt.enabled}
|
disabled={!settings.mqtt.enabled}
|
||||||
className="mt-1 block w-full rounded-lg border border-[#e5e5e5] bg-white/50 backdrop-blur-sm
|
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
|
px-3 py-2 text-[#1d1d1f] shadow-sm disabled:bg-gray-100 disabled:text-gray-500
|
||||||
focus:outline-none focus:ring-2 focus:ring-[#0071e3]/30 focus:border-[#0071e3]
|
focus:outline-none focus:ring-2 focus:ring-[#0071e3]/30 focus:border-[#0071e3]
|
||||||
disabled:bg-[#f5f5f7] disabled:text-[#86868b]
|
|
||||||
transition-colors duration-200"
|
transition-colors duration-200"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -198,34 +365,31 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||||
onChange={handleMQTTChange}
|
onChange={handleMQTTChange}
|
||||||
disabled={!settings.mqtt.enabled}
|
disabled={!settings.mqtt.enabled}
|
||||||
className="mt-1 block w-full rounded-lg border border-[#e5e5e5] bg-white/50 backdrop-blur-sm
|
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
|
px-3 py-2 text-[#1d1d1f] shadow-sm disabled:bg-gray-100 disabled:text-gray-500
|
||||||
focus:outline-none focus:ring-2 focus:ring-[#0071e3]/30 focus:border-[#0071e3]
|
focus:outline-none focus:ring-2 focus:ring-[#0071e3]/30 focus:border-[#0071e3]
|
||||||
disabled:bg-[#f5f5f7] disabled:text-[#86868b]
|
|
||||||
transition-colors duration-200"
|
transition-colors duration-200"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Tab.Panel>
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
|
|
||||||
{/* Dialog Actions */}
|
{/* Footer */}
|
||||||
<div className="mt-6 flex justify-end space-x-3">
|
<div className="flex items-center justify-end gap-3 p-6 border-t border-gray-200">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="px-4 py-2 text-sm font-medium text-[#1d1d1f] bg-[#f5f5f7] hover:bg-[#e5e5e5]
|
className="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-800
|
||||||
rounded-lg border border-[#e5e5e5] shadow-sm
|
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#0071e3]"
|
||||||
focus:outline-none focus:ring-2 focus:ring-[#0071e3]/30
|
|
||||||
transition-colors duration-200"
|
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-4 py-2 text-sm font-medium text-white bg-[#0071e3] hover:bg-[#0077ED]
|
className="px-4 py-2 text-sm font-medium text-white bg-[#0071e3] rounded-md
|
||||||
rounded-lg border border-transparent shadow-sm
|
hover:bg-[#0077ed] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#0071e3]"
|
||||||
focus:outline-none focus:ring-2 focus:ring-[#0071e3]/30
|
|
||||||
transition-colors duration-200"
|
|
||||||
>
|
>
|
||||||
Save Changes
|
Save Changes
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,23 @@ export interface MQTTSettings {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GPIOSettings {
|
||||||
|
gatePin: number;
|
||||||
|
statusPin: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoggingSettings {
|
||||||
|
level: string;
|
||||||
|
maxBytes: number;
|
||||||
|
backupCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
maxOpenTimeSeconds: string; // Open time in seconds
|
maxOpenTimeSeconds: number; // Open time in seconds
|
||||||
triggerDuration: string; // Trigger duration in milliseconds
|
triggerDuration: number; // Trigger duration in milliseconds
|
||||||
mqtt: MQTTSettings;
|
mqtt: MQTTSettings;
|
||||||
|
gpio: GPIOSettings;
|
||||||
|
logging: LoggingSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GateStatus {
|
export interface GateStatus {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue