From 7740be8bb514731cdc4e9e5753f6159b7b35d328 Mon Sep 17 00:00:00 2001 From: BluemediaGER Date: Sat, 13 Apr 2024 22:43:03 +0200 Subject: [PATCH] Reafctor app --- .gitignore | 1 - app/__init__.py | 0 {src => app}/database.py | 9 +- {src => app}/main.py | 8 +- app/models/__init__.py | 8 ++ {src => app}/models/chargepoint.py | 9 +- app/models/connector.py | 15 +++ app/models/id_token.py | 16 +++ app/models/meter_value.py | 17 +++ app/models/transaction.py | 20 +++ app/models/user.py | 14 +++ app/ocpp_proto/__init__.py | 0 app/ocpp_proto/chargepoint.py | 88 ++++++++++++++ .../ocpp_proto/chargepoint_manager.py | 17 +-- app/routers/__init__.py | 0 app/routers/chargepoint_v1.py | 114 ++++++++++++++++++ {src => app}/routers/ocpp_v1.py | 19 +-- app/schemas/__init__.py | 0 app/schemas/chargepoint.py | 31 +++++ {src => app}/schemas/connector.py | 3 +- {src => app}/schemas/id_token.py | 5 +- {src => app}/schemas/meter_value.py | 3 +- {src => app}/schemas/transaction.py | 3 +- {src => app}/schemas/user.py | 5 +- {src => app}/security.py | 0 app/util/__init__.py | 0 {src => app}/util/websocket_auth_backend.py | 19 ++- {src => app}/util/websocket_wrapper.py | 0 src/models/connector.py | 14 --- src/models/id_token.py | 14 --- src/models/meter_value.py | 16 --- src/models/transaction.py | 19 --- src/models/user.py | 12 -- src/ocpp_proto/chargepoint.py | 33 ----- src/routers/chargepoint_v1.py | 45 ------- src/schemas/chargepoint.py | 20 --- 36 files changed, 389 insertions(+), 208 deletions(-) create mode 100644 app/__init__.py rename {src => app}/database.py (81%) rename {src => app}/main.py (81%) create mode 100644 app/models/__init__.py rename {src => app}/models/chargepoint.py (54%) create mode 100644 app/models/connector.py create mode 100644 app/models/id_token.py create mode 100644 app/models/meter_value.py create mode 100644 app/models/transaction.py create mode 100644 app/models/user.py create mode 100644 app/ocpp_proto/__init__.py create mode 100644 app/ocpp_proto/chargepoint.py rename {src => app}/ocpp_proto/chargepoint_manager.py (56%) create mode 100644 app/routers/__init__.py create mode 100644 app/routers/chargepoint_v1.py rename {src => app}/routers/ocpp_v1.py (69%) create mode 100644 app/schemas/__init__.py create mode 100644 app/schemas/chargepoint.py rename {src => app}/schemas/connector.py (90%) rename {src => app}/schemas/id_token.py (77%) rename {src => app}/schemas/meter_value.py (98%) rename {src => app}/schemas/transaction.py (97%) rename {src => app}/schemas/user.py (76%) rename {src => app}/security.py (100%) create mode 100644 app/util/__init__.py rename {src => app}/util/websocket_auth_backend.py (54%) rename {src => app}/util/websocket_wrapper.py (100%) delete mode 100644 src/models/connector.py delete mode 100644 src/models/id_token.py delete mode 100644 src/models/meter_value.py delete mode 100644 src/models/transaction.py delete mode 100644 src/models/user.py delete mode 100644 src/ocpp_proto/chargepoint.py delete mode 100644 src/routers/chargepoint_v1.py delete mode 100644 src/schemas/chargepoint.py diff --git a/.gitignore b/.gitignore index 3b96008..9a5ab14 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ -**/__init__.py **/__pycache__ simple-ocpp-cs.db \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/database.py b/app/database.py similarity index 81% rename from src/database.py rename to app/database.py index de3e000..233eeb5 100644 --- a/src/database.py +++ b/app/database.py @@ -14,4 +14,11 @@ else: SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) -Base = declarative_base() \ No newline at end of file +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/src/main.py b/app/main.py similarity index 81% rename from src/main.py rename to app/main.py index ee9aaca..b9cbb02 100644 --- a/src/main.py +++ b/app/main.py @@ -2,9 +2,11 @@ from fastapi import FastAPI from starlette.middleware.authentication import AuthenticationMiddleware import uvicorn -from routers import chargepoint_v1, ocpp_v1 -from database import engine, Base -from util.websocket_auth_backend import BasicAuthBackend +from app.database import engine, Base +from app.models import * + +from app.routers import chargepoint_v1, ocpp_v1 +from app.util.websocket_auth_backend import BasicAuthBackend Base.metadata.create_all(bind=engine) diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..0f3f4ba --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,8 @@ +__all__ = [ + "chargepoint", + "connector", + "id_token", + "meter_value", + "transaction", + "user" +] \ No newline at end of file diff --git a/src/models/chargepoint.py b/app/models/chargepoint.py similarity index 54% rename from src/models/chargepoint.py rename to app/models/chargepoint.py index 8115257..3c5db63 100644 --- a/src/models/chargepoint.py +++ b/app/models/chargepoint.py @@ -1,15 +1,16 @@ -from sqlalchemy import Boolean, Column, DateTime, String +import uuid +from sqlalchemy import Uuid, Boolean, Column, DateTime, String from sqlalchemy.orm import relationship -from database import Base +from app.database import Base class ChargePoint(Base): __tablename__ = "chargepoints" - id = Column(String, primary_key=True) + id = Column(Uuid, primary_key=True, default=uuid.uuid4) friendly_name = Column(String, unique=True, index=True) is_active = Column(Boolean, default=True) password = Column(String) last_seen = Column(DateTime, nullable=True) - connectors = relationship("Connector") + connectors = relationship("Connector", cascade="delete, delete-orphan") diff --git a/app/models/connector.py b/app/models/connector.py new file mode 100644 index 0000000..97f168a --- /dev/null +++ b/app/models/connector.py @@ -0,0 +1,15 @@ +import uuid +from sqlalchemy import Uuid, Column, Enum, ForeignKey, Integer + +from app.schemas.connector import ConnectorStatus +from app.database import Base + +class Connector(Base): + __tablename__ = "connectors" + + id = Column(Uuid, primary_key=True, default=uuid.uuid4) + evse = Column(Integer) + index = Column(Integer) + status = Column(Enum(ConnectorStatus)) + + chargepoint_id = Column(Uuid, ForeignKey("chargepoints.id")) diff --git a/app/models/id_token.py b/app/models/id_token.py new file mode 100644 index 0000000..61651b2 --- /dev/null +++ b/app/models/id_token.py @@ -0,0 +1,16 @@ +import uuid +from sqlalchemy import Uuid, Boolean, Column, ForeignKey, String +from sqlalchemy.orm import relationship + +from app.database import Base + +class IdToken(Base): + __tablename__ = "id_tokens" + + id = Column(Uuid, primary_key=True, default=uuid.uuid4) + title = Column(String) + is_active = Column(Boolean, default=True) + token = Column(String, index=True) + + owner_id = Column(Uuid, ForeignKey("users.id")) + owner = relationship("User", back_populates="id_tokens") diff --git a/app/models/meter_value.py b/app/models/meter_value.py new file mode 100644 index 0000000..9cd0835 --- /dev/null +++ b/app/models/meter_value.py @@ -0,0 +1,17 @@ +import uuid +from sqlalchemy import Uuid, Column, DateTime, Enum, Float, ForeignKey, String + +from app.database import Base +from app.schemas.meter_value import Measurand, PhaseType + +class Transaction(Base): + __tablename__ = "meter_values" + + id = Column(Uuid, primary_key=True, default=uuid.uuid4) + timestamp = Column(DateTime, index=True) + measurand = Column(Enum(Measurand)) + phase_type = Column(Enum(PhaseType)) + unit = Column(String) + value = Column(Float) + + transaction_id = Column(Uuid, ForeignKey("transactions.id"), index=True) \ No newline at end of file diff --git a/app/models/transaction.py b/app/models/transaction.py new file mode 100644 index 0000000..18cc83c --- /dev/null +++ b/app/models/transaction.py @@ -0,0 +1,20 @@ +import uuid +from sqlalchemy import Uuid, Column, DateTime, Enum, Float, ForeignKey +from sqlalchemy.orm import relationship, backref + +from app.schemas.transaction import TransactionEventTriggerReason, TransactionStatus +from app.database import Base + +class Transaction(Base): + __tablename__ = "transactions" + + id = Column(Uuid, primary_key=True, default=uuid.uuid4) + status = Column(Enum(TransactionStatus), index=True) + started_at = Column(DateTime, index=True) + ended_at = Column(DateTime, nullable=True, index=True) + meter_start = Column(Float) + meter_end = Column(Float, nullable=True) + end_reason = Column(Enum(TransactionEventTriggerReason)) + + connector_id = Column(Uuid, ForeignKey("connectors.id"), index=True) + id_token_id = Column(Uuid, ForeignKey("id_tokens.id"), index= True) diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 0000000..339e5f7 --- /dev/null +++ b/app/models/user.py @@ -0,0 +1,14 @@ +import uuid +from sqlalchemy import Uuid, Boolean, Column, String +from sqlalchemy.orm import relationship + +from app.database import Base + +class User(Base): + __tablename__ = "users" + + id = Column(Uuid, primary_key=True, default=uuid.uuid4) + friendly_name = Column(String, unique=True, index=True) + is_active = Column(Boolean, default=True) + + id_tokens = relationship("IdToken", back_populates="owner", cascade="delete, delete-orphan") diff --git a/app/ocpp_proto/__init__.py b/app/ocpp_proto/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/ocpp_proto/chargepoint.py b/app/ocpp_proto/chargepoint.py new file mode 100644 index 0000000..d2accb2 --- /dev/null +++ b/app/ocpp_proto/chargepoint.py @@ -0,0 +1,88 @@ +from datetime import datetime, UTC +import os + +from ocpp.routing import on +from ocpp.v201 import ChargePoint as cp +from ocpp.v201 import call_result +from ocpp.v201.enums import Action, RegistrationStatusType, AuthorizationStatusType + +from app.database import SessionLocal +from app.models.chargepoint import ChargePoint +from app.models.connector import Connector +from app.models.id_token import IdToken +from app.schemas.connector import ConnectorStatus + +class ChargePoint(cp): + + @on(Action.BootNotification) + async def on_boot_notification(self, charging_station, reason, **kwargs): + with SessionLocal() as db: + db_chargepoint = db.query(ChargePoint).filter(ChargePoint.friendly_name == self.id).first() + db_chargepoint.last_seen = datetime.now(UTC) + db.commit() + return call_result.BootNotificationPayload( + current_time=datetime.now(UTC).isoformat(), + interval=int(os.getenv("CS_HEARTBEAT_INTERVAL", "1800")), + status=RegistrationStatusType.accepted + ) + + @on(Action.Heartbeat) + async def on_heartbeat_request(self): + with SessionLocal() as db: + db_chargepoint = db.query(ChargePoint).filter(ChargePoint.friendly_name == self.id).first() + db_chargepoint.last_seen = datetime.now(UTC) + db.commit() + return call_result.HeartbeatPayload( + current_time=datetime.now(UTC).isoformat() + ) + + @on(Action.StatusNotification) + async def on_status_notification(self, evse_id: int, connector_id: int, connector_status: str, **kwargs): + with SessionLocal() as db: + db_chargepoint = db.query(ChargePoint).filter(ChargePoint.friendly_name == self.id).first() + db_chargepoint.last_seen = datetime.now(UTC) + + db_connector = db.query(Connector).filter( + Connector.chargepoint_id == db_chargepoint.id, + Connector.evse == evse_id, + Connector.index == connector_id + ).first() + if db_connector == None: + db_connector = Connector( + chargepoint_id = db_chargepoint.id, + evse = evse_id, + index = connector_id, + status = ConnectorStatus(connector_status) + ) + db.add(db_connector) + else: + db_connector.status = ConnectorStatus(connector_status) + + db.commit() + + return call_result.StatusNotificationPayload() + + @on(Action.Authorize) + async def on_authorize(self, id_token, **kwargs): + if id_token == None: + return call_result.AuthorizePayload(id_token_info={'status': AuthorizationStatusType.invalid}) + if id_token.type != "ISO14443" | "ISO15693": + return call_result.AuthorizePayload(id_token_info={'status': AuthorizationStatusType.invalid}) + + with SessionLocal() as db: + db_chargepoint = db.query(ChargePoint).filter(ChargePoint.friendly_name == self.id).first() + db_chargepoint.last_seen = datetime.now(UTC) + + db_id_token = db.query(IdToken).filter(IdToken.token == id_token.id).first() + db.commit() + + if db_id_token == None: + return call_result.AuthorizePayload(id_token_info={'status': AuthorizationStatusType.unknown}) + if db_id_token.is_active == False: + return call_result.AuthorizePayload(id_token_info={'status': AuthorizationStatusType.blocked}) + + return call_result.AuthorizePayload(id_token_info={'status': AuthorizationStatusType.accepted, 'groupIdToken': str(db_id_token.owner_id)}) + + @on(Action.TransactionEvent) + async def on_transaction_event(self): + return call_result.TransactionEventPayload() diff --git a/src/ocpp_proto/chargepoint_manager.py b/app/ocpp_proto/chargepoint_manager.py similarity index 56% rename from src/ocpp_proto/chargepoint_manager.py rename to app/ocpp_proto/chargepoint_manager.py index 5035adb..60ebdc6 100644 --- a/src/ocpp_proto/chargepoint_manager.py +++ b/app/ocpp_proto/chargepoint_manager.py @@ -1,22 +1,23 @@ import logging from typing import Any, Coroutine, Dict +from uuid import UUID from websockets import ConnectionClosed -from ocpp_proto.chargepoint import ChargePoint +from app.ocpp_proto.chargepoint import ChargePoint -__active_connections: Dict[str, ChargePoint] = {} +__active_connections: Dict[UUID, ChargePoint] = {} -async def start(cp: ChargePoint): +async def start(id: UUID, cp: ChargePoint): try: - __active_connections[cp.id] = cp + __active_connections[id] = cp await cp.start() except ConnectionClosed: - logging.info("Charging station '%s' disconnected", cp.id) - __active_connections.pop(cp.id, None) + logging.info("Charging station '%s' (%s) disconnected", cp.id, id) + __active_connections.pop(id, None) async def call( - chargepoint_id: str, + chargepoint_id: UUID, payload: Any, suppress: bool = True, unique_id: Any | None = None @@ -27,5 +28,5 @@ async def call( except KeyError as e: raise e -def is_connected(chargepoint_id: str): +def is_connected(chargepoint_id: UUID): return chargepoint_id in __active_connections.keys() diff --git a/app/routers/__init__.py b/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routers/chargepoint_v1.py b/app/routers/chargepoint_v1.py new file mode 100644 index 0000000..cd768b5 --- /dev/null +++ b/app/routers/chargepoint_v1.py @@ -0,0 +1,114 @@ +import random +import string +from uuid import UUID +from fastapi import APIRouter, HTTPException, Security +from fastapi.params import Depends +from sqlalchemy.orm import Session + +from app.database import get_db +from app.ocpp_proto import chargepoint_manager +from app.schemas.chargepoint import ChargePoint, ChargePointCreate, ChargePointUpdate, ChargePointPassword, ChargePointConnectionInfo +from app.models.chargepoint import ChargePoint as DBChargePoint +from app.security import get_api_key + +router = APIRouter( + prefix="/chargepoint", + tags=["chargepoint"], +) + +@router.get(path="/", response_model=list[ChargePoint]) +async def get_chargepoints( + skip: int = 0, + limit: int = 20, + api_key: str = Security(get_api_key), + db: Session = Depends(get_db) +): + return db.query(DBChargePoint).offset(skip).limit(limit).all() + +@router.get(path="/{chargepoint_id}", response_model=ChargePoint) +async def get_chargepoint( + chargepoint_id: UUID, + api_key: str = Security(get_api_key), + db: Session = Depends(get_db) +): + chargepoint = db.query(DBChargePoint).filter(DBChargePoint.id == chargepoint_id).first() + if chargepoint is None: + raise HTTPException(status_code=404, detail="Chargepoint not found") + return chargepoint + +@router.get(path="/{chargepoint_id}/password", response_model=ChargePointPassword) +async def get_chargepoint_password( + chargepoint_id: UUID, + api_key: str = Security(get_api_key), + db: Session = Depends(get_db) +): + chargepoint = db.query(DBChargePoint).filter(DBChargePoint.id == chargepoint_id).first() + if chargepoint is None: + raise HTTPException(status_code=404, detail="Chargepoint not found") + return ChargePointPassword(password=chargepoint.password) + +@router.delete(path="/{chargepoint_id}/password", response_model=ChargePointPassword) +async def reset_chargepoint_password( + chargepoint_id: UUID, + api_key: str = Security(get_api_key), + db: Session = Depends(get_db) +): + chargepoint = db.query(DBChargePoint).filter(DBChargePoint.id == chargepoint_id).first() + if chargepoint is None: + raise HTTPException(status_code=404, detail="Chargepoint not found") + chargepoint.password = ''.join(random.choice(string.ascii_letters + string.digits) for i in range(24)) + db.commit() + return ChargePointPassword(password=chargepoint.password) + +@router.post(path="/", response_model=ChargePoint) +async def create_chargepoint( + chargepoint: ChargePointCreate, + api_key: str = Security(get_api_key), + db: Session = Depends(get_db) +): + chargepoint_db = DBChargePoint( + friendly_name=chargepoint.friendly_name, + is_active=chargepoint.is_active, + password=''.join(random.choice(string.ascii_letters + string.digits) for i in range(24)) + ) + db.add(chargepoint_db) + db.commit() + db.refresh(chargepoint_db) + return chargepoint_db + +@router.patch(path="/{chargepoint_id}", response_model=ChargePoint) +async def update_chargepoint( + chargepoint_id: UUID, + chargepoint_update: ChargePointUpdate, + api_key: str = Security(get_api_key), + db: Session = Depends(get_db) +): + chargepoint = db.query(DBChargePoint).filter(DBChargePoint.id == chargepoint_id).first() + if chargepoint is None: + raise HTTPException(status_code=404, detail="Chargepoint not found") + for key, value in chargepoint_update.model_dump(exclude_unset=True).items(): + setattr(chargepoint, key, value) + db.commit() + return chargepoint + +@router.delete(path="/{chargepoint_id}", response_model=[]) +async def delete_chargepoint( + chargepoint_id: UUID, + api_key: str = Security(get_api_key), + db: Session = Depends(get_db) +): + chargepoint = db.query(DBChargePoint).filter(DBChargePoint.id == chargepoint_id).first() + if chargepoint is None: + raise HTTPException(status_code=404, detail="Chargepoint not found") + db.delete(chargepoint) + db.commit() + return [] + +@router.get(path="/{chargepoint_id}/status", response_model=ChargePointConnectionInfo) +async def get_chargepoint_status( + chargepoint_id: UUID, + api_key: str = Security(get_api_key) +): + return ChargePointConnectionInfo( + connected=chargepoint_manager.is_connected(chargepoint_id) + ) diff --git a/src/routers/ocpp_v1.py b/app/routers/ocpp_v1.py similarity index 69% rename from src/routers/ocpp_v1.py rename to app/routers/ocpp_v1.py index 91e122a..3b8ca41 100644 --- a/src/routers/ocpp_v1.py +++ b/app/routers/ocpp_v1.py @@ -1,24 +1,25 @@ import logging from fastapi import APIRouter, WebSocket, WebSocketException -from ocpp_proto import chargepoint_manager -from ocpp_proto.chargepoint import ChargePoint -from util.websocket_wrapper import WebSocketWrapper + +from app.ocpp_proto import chargepoint_manager +from app.ocpp_proto.chargepoint import ChargePoint +from app.util.websocket_wrapper import WebSocketWrapper router = APIRouter() -@router.websocket("/{charging_station_id}") +@router.websocket("/{charging_station_friendly_name}") async def websocket_endpoint( *, websocket: WebSocket, - charging_station_id: str, + charging_station_friendly_name: str, ): """ For every new charging station that connects, create a ChargePoint instance and start listening for messages. """ - if (websocket.user.username != charging_station_id): + if (websocket.user.friendly_name != charging_station_friendly_name): raise WebSocketException(code=1008, reason="Username doesn't match chargepoint identifier") - logging.info("Charging station '%s' connected", charging_station_id) + logging.info("Charging station '%s' (%s) connected", charging_station_friendly_name, websocket.user.id) # Check protocols try: @@ -42,5 +43,5 @@ async def websocket_endpoint( # Accept connection and begin communication await websocket.accept(subprotocol="ocpp2.0.1") - cp = ChargePoint(charging_station_id, WebSocketWrapper(websocket)) - await chargepoint_manager.start(cp) + cp = ChargePoint(charging_station_friendly_name, WebSocketWrapper(websocket)) + await chargepoint_manager.start(websocket.user.id, cp) diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/schemas/chargepoint.py b/app/schemas/chargepoint.py new file mode 100644 index 0000000..acb30b3 --- /dev/null +++ b/app/schemas/chargepoint.py @@ -0,0 +1,31 @@ +from datetime import datetime +from typing import Optional +from uuid import UUID +from pydantic import BaseModel + +from app.schemas.connector import Connector + +class ChargePointBase(BaseModel): + friendly_name: str + is_active: bool + +class ChargePointUpdate(BaseModel): + friendly_name: Optional[str] = None + is_active: Optional[bool] = None + +class ChargePointCreate(ChargePointBase): + pass + +class ChargePoint(ChargePointBase): + id: UUID + last_seen: datetime | None + connectors: list[Connector] = [] + + class Config: + from_attributes = True + +class ChargePointPassword(BaseModel): + password: str + +class ChargePointConnectionInfo(BaseModel): + connected: bool diff --git a/src/schemas/connector.py b/app/schemas/connector.py similarity index 90% rename from src/schemas/connector.py rename to app/schemas/connector.py index 41a5645..786f9e7 100644 --- a/src/schemas/connector.py +++ b/app/schemas/connector.py @@ -1,4 +1,5 @@ import enum +from uuid import UUID from pydantic import BaseModel class ConnectorStatus(enum.Enum): @@ -9,7 +10,7 @@ class ConnectorStatus(enum.Enum): FAULTED = "Faulted" class Connector(BaseModel): - id: str + id: UUID evse: int index: int status: ConnectorStatus diff --git a/src/schemas/id_token.py b/app/schemas/id_token.py similarity index 77% rename from src/schemas/id_token.py rename to app/schemas/id_token.py index 2730a68..d279913 100644 --- a/src/schemas/id_token.py +++ b/app/schemas/id_token.py @@ -1,6 +1,7 @@ +from uuid import UUID from pydantic import BaseModel -from schemas.user import User +from app.schemas.user import User class IdTokenBase(BaseModel): title: str @@ -10,7 +11,7 @@ class IdTokenCreate(IdTokenBase): pass class IdToken(IdTokenBase): - id: str + id: UUID owner: User class Config: diff --git a/src/schemas/meter_value.py b/app/schemas/meter_value.py similarity index 98% rename from src/schemas/meter_value.py rename to app/schemas/meter_value.py index 830180f..5e31278 100644 --- a/src/schemas/meter_value.py +++ b/app/schemas/meter_value.py @@ -1,5 +1,6 @@ from datetime import datetime import enum +from uuid import UUID from pydantic import BaseModel class PhaseType(enum.Enum): @@ -42,7 +43,7 @@ class Measurand(enum.Enum): VOLTAGE = "Voltage" class MeterValue(BaseModel): - id: str + id: UUID timestamp: datetime measurand: Measurand phase_type: PhaseType diff --git a/src/schemas/transaction.py b/app/schemas/transaction.py similarity index 97% rename from src/schemas/transaction.py rename to app/schemas/transaction.py index 7723f6e..6937be7 100644 --- a/src/schemas/transaction.py +++ b/app/schemas/transaction.py @@ -1,5 +1,6 @@ from datetime import datetime import enum +from uuid import UUID from pydantic import BaseModel class TransactionStatus(enum.Enum): @@ -30,7 +31,7 @@ class TransactionEventTriggerReason(enum.Enum): RESET_COMMAND = "ResetCommand" class Transaction(BaseModel): - id: str + id: UUID status: TransactionStatus started_at: datetime ended_at: datetime diff --git a/src/schemas/user.py b/app/schemas/user.py similarity index 76% rename from src/schemas/user.py rename to app/schemas/user.py index eceedfe..c06817b 100644 --- a/src/schemas/user.py +++ b/app/schemas/user.py @@ -1,6 +1,7 @@ +from uuid import UUID from pydantic import BaseModel -from schemas.id_token import IdToken +from app.schemas.id_token import IdToken class UserBase(BaseModel): friendly_name: str @@ -10,7 +11,7 @@ class UserCreate(UserBase): pass class User(UserBase): - id: str + id: UUID id_tokens: list[IdToken] = [] class Config: diff --git a/src/security.py b/app/security.py similarity index 100% rename from src/security.py rename to app/security.py diff --git a/app/util/__init__.py b/app/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/util/websocket_auth_backend.py b/app/util/websocket_auth_backend.py similarity index 54% rename from src/util/websocket_auth_backend.py rename to app/util/websocket_auth_backend.py index 12802ea..a3260a6 100644 --- a/src/util/websocket_auth_backend.py +++ b/app/util/websocket_auth_backend.py @@ -1,9 +1,13 @@ import base64 import binascii +from uuid import UUID from starlette.authentication import ( AuthCredentials, AuthenticationBackend, AuthenticationError, SimpleUser ) +from app.database import SessionLocal +from app.models.chargepoint import ChargePoint + class BasicAuthBackend(AuthenticationBackend): async def authenticate(self, conn): if "Authorization" not in conn.headers: @@ -19,5 +23,16 @@ class BasicAuthBackend(AuthenticationBackend): raise AuthenticationError('Invalid basic auth credentials') username, _, password = decoded.partition(":") - # TODO: You'd want to verify the username and password here. - return AuthCredentials(["authenticated"]), SimpleUser(username) \ No newline at end of file + try: + id = UUID(username) + except (ValueError) as exc: + raise AuthenticationError('Invalid basic auth credentials') + + with SessionLocal() as db: + chargepoint = db.query(ChargePoint).filter(ChargePoint.id == id).first() + if chargepoint is None: + raise AuthenticationError('Invalid basic auth credentials') + if chargepoint.password != password: + raise AuthenticationError('Invalid basic auth credentials') + + return AuthCredentials(["authenticated"]), chargepoint \ No newline at end of file diff --git a/src/util/websocket_wrapper.py b/app/util/websocket_wrapper.py similarity index 100% rename from src/util/websocket_wrapper.py rename to app/util/websocket_wrapper.py diff --git a/src/models/connector.py b/src/models/connector.py deleted file mode 100644 index 436faf2..0000000 --- a/src/models/connector.py +++ /dev/null @@ -1,14 +0,0 @@ -from sqlalchemy import Column, Enum, ForeignKey, Integer, String - -from schemas.connector import ConnectorStatus -from database import Base - -class Connector(Base): - __tablename__ = "connectors" - - id = Column(String, primary_key=True) - evse = Column(Integer) - index = Column(Integer) - status = Column(Enum(ConnectorStatus)) - - chargepoint_id = Column(String, ForeignKey("chargepoints.id")) diff --git a/src/models/id_token.py b/src/models/id_token.py deleted file mode 100644 index 32ead9d..0000000 --- a/src/models/id_token.py +++ /dev/null @@ -1,14 +0,0 @@ -from sqlalchemy import Boolean, Column, ForeignKey, String -from sqlalchemy.orm import relationship - -from database import Base - -class IdToken(Base): - __tablename__ = "id_tokens" - - id = Column(String, primary_key=True) - title = Column(String) - is_active = Column(Boolean, default=True) - - owner_id = Column(String, ForeignKey("users.id")) - owner = relationship("User", back_populates="id_tokens") diff --git a/src/models/meter_value.py b/src/models/meter_value.py deleted file mode 100644 index 3fe9ff4..0000000 --- a/src/models/meter_value.py +++ /dev/null @@ -1,16 +0,0 @@ -from sqlalchemy import Column, DateTime, Enum, Float, ForeignKey, String - -from database import Base -from schemas.meter_value import Measurand, PhaseType - -class Transaction(Base): - __tablename__ = "meter_values" - - id = Column(String, primary_key=True) - timestamp = Column(DateTime, index=True) - measurand = Column(Enum(Measurand)) - phase_type = Column(Enum(PhaseType)) - unit = Column(String) - value = Column(Float) - - transaction_id = Column(String, ForeignKey("transactions.id", index=True)) \ No newline at end of file diff --git a/src/models/transaction.py b/src/models/transaction.py deleted file mode 100644 index d7ef02c..0000000 --- a/src/models/transaction.py +++ /dev/null @@ -1,19 +0,0 @@ -import enum -from sqlalchemy import Column, DateTime, Enum, Float, ForeignKey, String - -from schemas.transaction import TransactionEventTriggerReason, TransactionStatus -from database import Base - -class Transaction(Base): - __tablename__ = "transactions" - - id = Column(String, primary_key=True) - status = Column(Enum(TransactionStatus), index=True) - started_at = Column(DateTime, index=True) - ended_at = Column(DateTime, nullable=True, index=True) - meter_start = Column(Float) - meter_end = Column(Float, nullable=True) - end_reason = Column(Enum(TransactionEventTriggerReason)) - - connector_id = Column(String, ForeignKey("connectors.id", index=True)) - id_token_id = Column(String, ForeignKey("id_tokens.id", index= True)) diff --git a/src/models/user.py b/src/models/user.py deleted file mode 100644 index 8925954..0000000 --- a/src/models/user.py +++ /dev/null @@ -1,12 +0,0 @@ -from sqlalchemy import Boolean, Column, String -from sqlalchemy.orm import relationship - -from database import Base - -class User(Base): - __tablename__ = "users" - - id = Column(String, primary_key=True) - friendly_name = Column(String, unique=True, index=True) - is_active = Column(Boolean, default=True) - id_tokens = relationship("IdToken", back_populates="owner") diff --git a/src/ocpp_proto/chargepoint.py b/src/ocpp_proto/chargepoint.py deleted file mode 100644 index bcdda6a..0000000 --- a/src/ocpp_proto/chargepoint.py +++ /dev/null @@ -1,33 +0,0 @@ -from datetime import datetime, UTC -import os -from ocpp.routing import on -from ocpp.v201 import ChargePoint as cp -from ocpp.v201 import call_result -from ocpp.v201.enums import Action, RegistrationStatusType, AuthorizationStatusType - -class ChargePoint(cp): - @on(Action.BootNotification) - async def on_boot_notification(self, charging_station, reason, **kwargs): - return call_result.BootNotificationPayload( - current_time=datetime.now(UTC).isoformat(), - interval=int(os.getenv("CS_HEARTBEAT_INTERVAL", "1800")), - status=RegistrationStatusType.accepted - ) - - @on(Action.Heartbeat) - async def on_heartbeat_request(self): - return call_result.HeartbeatPayload( - current_time=datetime.now(UTC).isoformat() - ) - - @on(Action.StatusNotification) - async def on_status_notification(self, evse_id, connector_id, connector_status, **kwargs): - return call_result.StatusNotificationPayload() - - @on(Action.Authorize) - async def on_authorize(self): - return call_result.AuthorizePayload(id_token_info={'status': AuthorizationStatusType.accepted, 'groupIdToken': ""}) - - @on(Action.TransactionEvent) - async def on_transaction_event(self): - return call_result.TransactionEventPayload() diff --git a/src/routers/chargepoint_v1.py b/src/routers/chargepoint_v1.py deleted file mode 100644 index c9ff823..0000000 --- a/src/routers/chargepoint_v1.py +++ /dev/null @@ -1,45 +0,0 @@ -from fastapi import APIRouter, Security -from ocpp_proto import chargepoint_manager -from schemas.chargepoint import ChargePoint -from security import get_api_key - -router = APIRouter( - prefix="/chargepoint", - tags=["chargepoint"], -) - -@router.get(path="/", response_model=list[ChargePoint]) -async def get_chargepoints(api_key: str = Security(get_api_key)): - return - -@router.post(path="/", response_model=ChargePoint) -async def create_chargepoint(api_key: str = Security(get_api_key)): - return - -@router.get(path="/{chargepoint_id}", response_model=ChargePoint) -async def get_chargepoint( - chargepoint_id: str, - api_key: str = Security(get_api_key), -): - return - -@router.patch(path="/{chargepoint_id}", response_model=ChargePoint) -async def update_chargepoint( - chargepoint_id: str, - api_key: str = Security(get_api_key), -): - return - -@router.delete(path="/{chargepoint_id}", response_model=[]) -async def delete_chargepoint( - chargepoint_id: str, - api_key: str = Security(get_api_key), -): - return - -@router.get(path="/{chargepoint_id}/status", response_model=bool) -async def get_chargepoint( - chargepoint_id: str, - api_key: str = Security(get_api_key), -): - return chargepoint_manager.is_connected(chargepoint_id) diff --git a/src/schemas/chargepoint.py b/src/schemas/chargepoint.py deleted file mode 100644 index 5c4aaad..0000000 --- a/src/schemas/chargepoint.py +++ /dev/null @@ -1,20 +0,0 @@ -from datetime import datetime -from pydantic import BaseModel - -from schemas.connector import Connector - -class ChargePointBase(BaseModel): - friendly_name: str - is_active: bool - -class ChargePointCreate(ChargePointBase): - pass - -class ChargePoint(ChargePointBase): - id: str - password: str - last_seen: datetime - connectors: list[Connector] = [] - - class Config: - from_attributes = True \ No newline at end of file