Database¶
LightningROD uses PostgreSQL 16 with SQLAlchemy 2.0 in async mode. The schema covers the full ha-fordpass data model plus reference tables for networks, locations, stalls, and settings.
Schema Overview¶
Core Tables¶
| Table | Purpose |
|---|---|
ev_charging_session |
Charging events: timing, energy, cost, location, SOC, EVSE data |
ev_battery_status |
HV/12V battery snapshots: SOC, voltage, current, temperature |
ev_vehicle_status |
Drivetrain, temperatures, tire pressure, door locks |
ev_trip_metrics |
Per-trip energy, distance, efficiency, driving scores |
ev_location |
GPS snapshots with optional reverse geocoding |
Reference Tables¶
| Table | Purpose |
|---|---|
ev_charging_networks |
Network definitions with cost_per_kwh and color |
ev_location_lookup |
Known locations with network FK and optional cost override |
ev_charger_stalls |
Charger configurations per location (type, rated kW, connector) |
app_settings |
Key-value store for user preferences and toggles |
ev_statistics |
Aggregate statistics summary (single row, recomputed) |
Key Relationships¶
ev_charging_networks
├── ev_location_lookup (network_id FK)
│ └── ev_charger_stalls (location_id FK)
└── ev_charging_session (network_id FK, stall_id FK)
Charging Session Fields¶
The ev_charging_session table includes:
- Identity:
id,session_id(UUID),device_id - Type/Location:
charge_type,location_name,location_type,network_id,location_id,is_free - Power:
charging_voltage,charging_amperage,charging_kw,max_power,min_power - Timestamps:
session_start_utc,session_end_utc,estimated_end_utc,recorded_at - Duration:
charge_duration_seconds,plugged_in_duration_seconds - Energy/SOC:
start_soc,end_soc,energy_kwh,miles_added - Cost:
cost,cost_source,estimated_cost,cost_without_overrides - EVSE:
evse_voltage,evse_amperage,evse_kw,evse_energy_kwh,evse_max_power_kw,charger_rated_kw,evse_source,stall_id - Location data:
address,latitude,longitude - Metadata:
source_system,ingested_at,original_timestamp
Connection Management¶
db/engine.py
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from config import settings
engine = create_async_engine(
settings.database_url,
pool_pre_ping=True,
pool_recycle=3600,
echo=settings.debug,
)
AsyncSessionLocal = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
)
Key settings:
pool_pre_ping=True-- validates connections before usepool_recycle=3600-- refreshes connections every hourasyncpgdriver for native async PostgreSQL access
Models¶
Models live in db/models/ with one file per domain area. The __init__.py imports all model classes so Alembic's autogenerate discovers every table:
db/models/__init__.py
# Import Base and ALL model classes so Alembic's autogenerate sees all tables.
# CRITICAL: Every model module must be imported here. If a module is missing,
# alembic revision --autogenerate will produce an empty migration.
from db.models.base import Base
from db.models.battery_status import EVBatteryStatus
from db.models.charging_session import EVChargingSession
from db.models.location import EVLocation
from db.models.reference import EVChargingNetwork, EVChargerStall, EVLocationLookup, EVStatistics, AppSettings
from db.models.trip_metrics import EVTripMetrics
from db.models.vehicle_status import EVVehicleStatus
__all__ = [
"Base",
"EVChargingSession",
"EVBatteryStatus",
"EVTripMetrics",
"EVLocation",
"EVVehicleStatus",
"EVChargingNetwork",
"EVChargerStall",
"EVLocationLookup",
"EVStatistics",
"AppSettings",
]
Migrations¶
Alembic manages schema versioning with an async-compatible env.py.
Creating a New Migration¶
Dependency Injection¶
Each request gets its own database session via FastAPI's dependency system:
web/dependencies.py
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession
from db.engine import AsyncSessionLocal
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
try:
yield session
finally:
await session.close()
Route handlers declare the dependency:
@router.get("/sessions")
async def sessions(request: Request, db: AsyncSession = Depends(get_db)):
...
The session is automatically committed on success and closed after the request.