From e5ae0bd58ea0f96d4ee7c3852be6418c511e7efd Mon Sep 17 00:00:00 2001 From: BluemediaGER Date: Sun, 14 Apr 2024 17:34:46 +0200 Subject: [PATCH 1/3] Add IdToken router --- app/main.py | 7 ++- app/models/id_token.py | 2 +- app/routers/chargepoint_v1.py | 4 +- app/routers/id_token_v1.py | 96 +++++++++++++++++++++++++++++++++++ app/routers/user_v1.py | 4 +- app/schemas/id_token.py | 11 +++- 6 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 app/routers/id_token_v1.py diff --git a/app/main.py b/app/main.py index 2e2a539..5b7dfbd 100644 --- a/app/main.py +++ b/app/main.py @@ -1,11 +1,13 @@ -from fastapi import FastAPI +from fastapi import FastAPI, Request +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse from starlette.middleware.authentication import AuthenticationMiddleware import uvicorn from app.database import engine, Base from app.models import * -from app.routers import chargepoint_v1, user_v1, ocpp_v1 +from app.routers import chargepoint_v1, id_token_v1, ocpp_v1, user_v1 from app.util.websocket_auth_backend import BasicAuthBackend Base.metadata.create_all(bind=engine) @@ -25,6 +27,7 @@ def create_app(): ) app.include_router(chargepoint_v1.router, prefix="/v1") + app.include_router(id_token_v1.router, prefix="/v1") app.include_router(user_v1.router, prefix="/v1") app.mount(path="/v1/ocpp", app=create_ocpp_app()) diff --git a/app/models/id_token.py b/app/models/id_token.py index 61651b2..12430bd 100644 --- a/app/models/id_token.py +++ b/app/models/id_token.py @@ -8,7 +8,7 @@ class IdToken(Base): __tablename__ = "id_tokens" id = Column(Uuid, primary_key=True, default=uuid.uuid4) - title = Column(String) + friendly_name = Column(String) is_active = Column(Boolean, default=True) token = Column(String, index=True) diff --git a/app/routers/chargepoint_v1.py b/app/routers/chargepoint_v1.py index 01d2b72..35d78f1 100644 --- a/app/routers/chargepoint_v1.py +++ b/app/routers/chargepoint_v1.py @@ -12,8 +12,8 @@ from app.models.chargepoint import ChargePoint as DbChargePoint from app.security import get_api_key router = APIRouter( - prefix="/chargepoint", - tags=["chargepoint (v1)"], + prefix="/chargepoints", + tags=["Chargepoint (v1)"], ) @router.get(path="", response_model=list[ChargePoint]) diff --git a/app/routers/id_token_v1.py b/app/routers/id_token_v1.py new file mode 100644 index 0000000..017aaaa --- /dev/null +++ b/app/routers/id_token_v1.py @@ -0,0 +1,96 @@ +from uuid import UUID +from fastapi import APIRouter, HTTPException, Security +from fastapi.exceptions import RequestValidationError +from fastapi.params import Depends +from sqlalchemy.orm import Session + +from app.database import get_db +from app.schemas.id_token import IdToken, IdTokenCreate, IdTokenUpdate +from app.models.id_token import IdToken as DbIdToken +from app.models.user import User as DbUser +from app.security import get_api_key + +router = APIRouter( + prefix="/id-tokens", + tags=["IdToken (v1)"] +) + +@router.get(path="", response_model=list[IdToken]) +async def get_it_tokens( + skip: int = 0, + limit: int = 20, + api_key: str = Security(get_api_key), + db: Session = Depends(get_db) +): + return db.query(DbIdToken).offset(skip).limit(limit).all() + +@router.get(path="/{id_token_id}", response_model=IdToken) +async def get_id_token( + id_token_id: UUID, + api_key: str = Security(get_api_key), + db: Session = Depends(get_db) +): + id_token = db.get(DbIdToken, id_token_id) + if id_token == None: + raise HTTPException(status_code=404, detail="IdToken not found") + return id_token + +@router.post(path="", status_code=201, response_model=IdToken) +async def create_id_token( + create_id_token: IdTokenCreate, + api_key: str = Security(get_api_key), + db: Session = Depends(get_db) +): + owner = db.get(DbUser, create_id_token.owner_id) + if owner == None: + raise HTTPException(status_code=422, detail=[{ + "loc": ["body", "owner_id"], + "msg": "Owner not found", + "type": "invalid_relation" + }]) + id_token = DbIdToken( + friendly_name=create_id_token.friendly_name, + is_active=create_id_token.is_active, + token=create_id_token.token, + owner_id=create_id_token.owner_id + ) + db.add(id_token) + db.commit() + db.refresh(id_token) + return id_token + +@router.patch(path="/{id_token_id}", response_model=IdToken) +async def update_id_token( + id_token_id: UUID, + id_token_update: IdTokenUpdate, + api_key: str = Security(get_api_key), + db: Session = Depends(get_db) +): + id_token = db.get(DbIdToken, id_token_id) + if id_token is None: + raise HTTPException(status_code=404, detail="IdToken not found") + for key, value in id_token_update.model_dump(exclude_unset=True).items(): + if key == "owner_id": + owner = db.get(DbUser, value) + if owner == None: + raise HTTPException(status_code=422, detail=[{ + "loc": ["body", "owner_id"], + "msg": "Owner not found", + "type": "invalid_relation" + }]) + setattr(id_token, key, value) + db.commit() + return id_token + +@router.delete(path="/{id_token_id}", response_model=None) +async def delete_id_token( + id_token_id: UUID, + api_key: str = Security(get_api_key), + db: Session = Depends(get_db) +): + id_token = db.get(DbIdToken, id_token_id) + if id_token == None: + raise HTTPException(status_code=404, detail="IdToken not found") + db.delete(id_token) + db.commit() + return [] diff --git a/app/routers/user_v1.py b/app/routers/user_v1.py index 064bc78..5655dad 100644 --- a/app/routers/user_v1.py +++ b/app/routers/user_v1.py @@ -9,8 +9,8 @@ from app.models.user import User as DbUser from app.security import get_api_key router = APIRouter( - prefix="/user", - tags=["user (v1)"], + prefix="/users", + tags=["User (v1)"], ) @router.get(path="", response_model=list[User]) diff --git a/app/schemas/id_token.py b/app/schemas/id_token.py index d279913..b8c5e06 100644 --- a/app/schemas/id_token.py +++ b/app/schemas/id_token.py @@ -1,18 +1,25 @@ +from typing import Optional from uuid import UUID from pydantic import BaseModel from app.schemas.user import User class IdTokenBase(BaseModel): - title: str + friendly_name: str is_active: bool + owner_id: UUID + token: str class IdTokenCreate(IdTokenBase): pass +class IdTokenUpdate(BaseModel): + friendly_name: Optional[str] = None + is_active: Optional[bool] = None + owner_id: Optional[UUID] = None + class IdToken(IdTokenBase): id: UUID - owner: User class Config: from_attributes = True \ No newline at end of file From d25f7f98382e4245a5f8f8a8d6a76a592b48f582 Mon Sep 17 00:00:00 2001 From: BluemediaGER Date: Sun, 14 Apr 2024 17:35:36 +0200 Subject: [PATCH 2/3] Fix chargepoint implementation --- app/ocpp_proto/chargepoint.py | 49 +++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/app/ocpp_proto/chargepoint.py b/app/ocpp_proto/chargepoint.py index d2accb2..f2fb317 100644 --- a/app/ocpp_proto/chargepoint.py +++ b/app/ocpp_proto/chargepoint.py @@ -4,10 +4,11 @@ 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 ocpp.v201.datatypes import IdTokenInfoType, IdTokenType +from ocpp.v201.enums import Action, RegistrationStatusType, AuthorizationStatusType, IdTokenType as IdTokenEnumType from app.database import SessionLocal -from app.models.chargepoint import ChargePoint +from app.models.chargepoint import ChargePoint as DbChargePoint from app.models.connector import Connector from app.models.id_token import IdToken from app.schemas.connector import ConnectorStatus @@ -17,7 +18,7 @@ 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 = db.query(DbChargePoint).filter(DbChargePoint.friendly_name == self.id).first() db_chargepoint.last_seen = datetime.now(UTC) db.commit() return call_result.BootNotificationPayload( @@ -29,7 +30,7 @@ class ChargePoint(cp): @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 = db.query(DbChargePoint).filter(DbChargePoint.friendly_name == self.id).first() db_chargepoint.last_seen = datetime.now(UTC) db.commit() return call_result.HeartbeatPayload( @@ -39,7 +40,7 @@ class ChargePoint(cp): @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 = db.query(DbChargePoint).filter(DbChargePoint.friendly_name == self.id).first() db_chargepoint.last_seen = datetime.now(UTC) db_connector = db.query(Connector).filter( @@ -64,24 +65,38 @@ class ChargePoint(cp): @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}) + if id_token["type"] not in ["ISO14443", "ISO15693"]: + return call_result.AuthorizePayload( + id_token_info=IdTokenInfoType( + status=AuthorizationStatusType.invalid + ) + ) with SessionLocal() as db: - db_chargepoint = db.query(ChargePoint).filter(ChargePoint.friendly_name == self.id).first() + db_chargepoint = db.query(DbChargePoint).filter(DbChargePoint.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_id_token = db.query(IdToken).filter(IdToken.token == id_token["id_token"]).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)}) + if db_id_token == None: + id_token_info=IdTokenInfoType( + status=AuthorizationStatusType.unknown + ) + else: + if db_id_token.is_active == False: + id_token_info=IdTokenInfoType( + status=AuthorizationStatusType.blocked + ) + else: + id_token_info=IdTokenInfoType( + status=AuthorizationStatusType.accepted, + group_id_token=IdTokenType( + type=IdTokenEnumType.central, + id_token=str(db_id_token.owner_id) + ) + ) + return call_result.AuthorizePayload(id_token_info) @on(Action.TransactionEvent) async def on_transaction_event(self): From 7780f247fb6519976baa8d9840b59d1f15cb3395 Mon Sep 17 00:00:00 2001 From: BluemediaGER Date: Sun, 14 Apr 2024 17:55:26 +0200 Subject: [PATCH 3/3] Add additional metadata to chargepoints --- app/models/chargepoint.py | 5 +++++ app/ocpp_proto/chargepoint.py | 5 ++++- app/schemas/chargepoint.py | 4 ++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/models/chargepoint.py b/app/models/chargepoint.py index 3c5db63..417431e 100644 --- a/app/models/chargepoint.py +++ b/app/models/chargepoint.py @@ -11,6 +11,11 @@ class ChargePoint(Base): friendly_name = Column(String, unique=True, index=True) is_active = Column(Boolean, default=True) password = Column(String) + last_seen = Column(DateTime, nullable=True) + vendor_name = Column(String, nullable=True) + model = Column(String, nullable=True) + serial_number = Column(String, nullable=True) + firmware_version = Column(String, nullable=True) connectors = relationship("Connector", cascade="delete, delete-orphan") diff --git a/app/ocpp_proto/chargepoint.py b/app/ocpp_proto/chargepoint.py index f2fb317..7eab494 100644 --- a/app/ocpp_proto/chargepoint.py +++ b/app/ocpp_proto/chargepoint.py @@ -16,10 +16,13 @@ from app.schemas.connector import ConnectorStatus class ChargePoint(cp): @on(Action.BootNotification) - async def on_boot_notification(self, charging_station, reason, **kwargs): + async def on_boot_notification(self, charging_station, **kwargs): with SessionLocal() as db: db_chargepoint = db.query(DbChargePoint).filter(DbChargePoint.friendly_name == self.id).first() db_chargepoint.last_seen = datetime.now(UTC) + for key in charging_station.keys(): + if key in db_chargepoint.__dict__: + setattr(db_chargepoint, key, charging_station[key]) db.commit() return call_result.BootNotificationPayload( current_time=datetime.now(UTC).isoformat(), diff --git a/app/schemas/chargepoint.py b/app/schemas/chargepoint.py index acb30b3..a16db84 100644 --- a/app/schemas/chargepoint.py +++ b/app/schemas/chargepoint.py @@ -19,6 +19,10 @@ class ChargePointCreate(ChargePointBase): class ChargePoint(ChargePointBase): id: UUID last_seen: datetime | None + vendor_name: str | None + model: str | None + serial_number: str | None + firmware_version: str | None connectors: list[Connector] = [] class Config: