Reafctor app
This commit is contained in:
parent
b8216f6ade
commit
7740be8bb5
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,2 @@
|
||||||
**/__init__.py
|
|
||||||
**/__pycache__
|
**/__pycache__
|
||||||
simple-ocpp-cs.db
|
simple-ocpp-cs.db
|
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
|
@ -14,4 +14,11 @@ else:
|
||||||
|
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
|
@ -2,9 +2,11 @@ from fastapi import FastAPI
|
||||||
from starlette.middleware.authentication import AuthenticationMiddleware
|
from starlette.middleware.authentication import AuthenticationMiddleware
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
from routers import chargepoint_v1, ocpp_v1
|
from app.database import engine, Base
|
||||||
from database import engine, Base
|
from app.models import *
|
||||||
from util.websocket_auth_backend import BasicAuthBackend
|
|
||||||
|
from app.routers import chargepoint_v1, ocpp_v1
|
||||||
|
from app.util.websocket_auth_backend import BasicAuthBackend
|
||||||
|
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
8
app/models/__init__.py
Normal file
8
app/models/__init__.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
__all__ = [
|
||||||
|
"chargepoint",
|
||||||
|
"connector",
|
||||||
|
"id_token",
|
||||||
|
"meter_value",
|
||||||
|
"transaction",
|
||||||
|
"user"
|
||||||
|
]
|
|
@ -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 sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from database import Base
|
from app.database import Base
|
||||||
|
|
||||||
class ChargePoint(Base):
|
class ChargePoint(Base):
|
||||||
__tablename__ = "chargepoints"
|
__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)
|
friendly_name = Column(String, unique=True, index=True)
|
||||||
is_active = Column(Boolean, default=True)
|
is_active = Column(Boolean, default=True)
|
||||||
password = Column(String)
|
password = Column(String)
|
||||||
last_seen = Column(DateTime, nullable=True)
|
last_seen = Column(DateTime, nullable=True)
|
||||||
|
|
||||||
connectors = relationship("Connector")
|
connectors = relationship("Connector", cascade="delete, delete-orphan")
|
15
app/models/connector.py
Normal file
15
app/models/connector.py
Normal file
|
@ -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"))
|
16
app/models/id_token.py
Normal file
16
app/models/id_token.py
Normal file
|
@ -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")
|
17
app/models/meter_value.py
Normal file
17
app/models/meter_value.py
Normal file
|
@ -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)
|
20
app/models/transaction.py
Normal file
20
app/models/transaction.py
Normal file
|
@ -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)
|
14
app/models/user.py
Normal file
14
app/models/user.py
Normal file
|
@ -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")
|
0
app/ocpp_proto/__init__.py
Normal file
0
app/ocpp_proto/__init__.py
Normal file
88
app/ocpp_proto/chargepoint.py
Normal file
88
app/ocpp_proto/chargepoint.py
Normal file
|
@ -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()
|
|
@ -1,22 +1,23 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Coroutine, Dict
|
from typing import Any, Coroutine, Dict
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from websockets import ConnectionClosed
|
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:
|
try:
|
||||||
__active_connections[cp.id] = cp
|
__active_connections[id] = cp
|
||||||
await cp.start()
|
await cp.start()
|
||||||
except ConnectionClosed:
|
except ConnectionClosed:
|
||||||
logging.info("Charging station '%s' disconnected", cp.id)
|
logging.info("Charging station '%s' (%s) disconnected", cp.id, id)
|
||||||
__active_connections.pop(cp.id, None)
|
__active_connections.pop(id, None)
|
||||||
|
|
||||||
async def call(
|
async def call(
|
||||||
chargepoint_id: str,
|
chargepoint_id: UUID,
|
||||||
payload: Any,
|
payload: Any,
|
||||||
suppress: bool = True,
|
suppress: bool = True,
|
||||||
unique_id: Any | None = None
|
unique_id: Any | None = None
|
||||||
|
@ -27,5 +28,5 @@ async def call(
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def is_connected(chargepoint_id: str):
|
def is_connected(chargepoint_id: UUID):
|
||||||
return chargepoint_id in __active_connections.keys()
|
return chargepoint_id in __active_connections.keys()
|
0
app/routers/__init__.py
Normal file
0
app/routers/__init__.py
Normal file
114
app/routers/chargepoint_v1.py
Normal file
114
app/routers/chargepoint_v1.py
Normal file
|
@ -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)
|
||||||
|
)
|
|
@ -1,24 +1,25 @@
|
||||||
import logging
|
import logging
|
||||||
from fastapi import APIRouter, WebSocket, WebSocketException
|
from fastapi import APIRouter, WebSocket, WebSocketException
|
||||||
from ocpp_proto import chargepoint_manager
|
|
||||||
from ocpp_proto.chargepoint import ChargePoint
|
from app.ocpp_proto import chargepoint_manager
|
||||||
from util.websocket_wrapper import WebSocketWrapper
|
from app.ocpp_proto.chargepoint import ChargePoint
|
||||||
|
from app.util.websocket_wrapper import WebSocketWrapper
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.websocket("/{charging_station_id}")
|
@router.websocket("/{charging_station_friendly_name}")
|
||||||
async def websocket_endpoint(
|
async def websocket_endpoint(
|
||||||
*,
|
*,
|
||||||
websocket: WebSocket,
|
websocket: WebSocket,
|
||||||
charging_station_id: str,
|
charging_station_friendly_name: str,
|
||||||
):
|
):
|
||||||
""" For every new charging station that connects, create a ChargePoint
|
""" For every new charging station that connects, create a ChargePoint
|
||||||
instance and start listening for messages.
|
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")
|
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
|
# Check protocols
|
||||||
try:
|
try:
|
||||||
|
@ -42,5 +43,5 @@ async def websocket_endpoint(
|
||||||
|
|
||||||
# Accept connection and begin communication
|
# Accept connection and begin communication
|
||||||
await websocket.accept(subprotocol="ocpp2.0.1")
|
await websocket.accept(subprotocol="ocpp2.0.1")
|
||||||
cp = ChargePoint(charging_station_id, WebSocketWrapper(websocket))
|
cp = ChargePoint(charging_station_friendly_name, WebSocketWrapper(websocket))
|
||||||
await chargepoint_manager.start(cp)
|
await chargepoint_manager.start(websocket.user.id, cp)
|
0
app/schemas/__init__.py
Normal file
0
app/schemas/__init__.py
Normal file
31
app/schemas/chargepoint.py
Normal file
31
app/schemas/chargepoint.py
Normal file
|
@ -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
|
|
@ -1,4 +1,5 @@
|
||||||
import enum
|
import enum
|
||||||
|
from uuid import UUID
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
class ConnectorStatus(enum.Enum):
|
class ConnectorStatus(enum.Enum):
|
||||||
|
@ -9,7 +10,7 @@ class ConnectorStatus(enum.Enum):
|
||||||
FAULTED = "Faulted"
|
FAULTED = "Faulted"
|
||||||
|
|
||||||
class Connector(BaseModel):
|
class Connector(BaseModel):
|
||||||
id: str
|
id: UUID
|
||||||
evse: int
|
evse: int
|
||||||
index: int
|
index: int
|
||||||
status: ConnectorStatus
|
status: ConnectorStatus
|
|
@ -1,6 +1,7 @@
|
||||||
|
from uuid import UUID
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from schemas.user import User
|
from app.schemas.user import User
|
||||||
|
|
||||||
class IdTokenBase(BaseModel):
|
class IdTokenBase(BaseModel):
|
||||||
title: str
|
title: str
|
||||||
|
@ -10,7 +11,7 @@ class IdTokenCreate(IdTokenBase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class IdToken(IdTokenBase):
|
class IdToken(IdTokenBase):
|
||||||
id: str
|
id: UUID
|
||||||
owner: User
|
owner: User
|
||||||
|
|
||||||
class Config:
|
class Config:
|
|
@ -1,5 +1,6 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import enum
|
import enum
|
||||||
|
from uuid import UUID
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
class PhaseType(enum.Enum):
|
class PhaseType(enum.Enum):
|
||||||
|
@ -42,7 +43,7 @@ class Measurand(enum.Enum):
|
||||||
VOLTAGE = "Voltage"
|
VOLTAGE = "Voltage"
|
||||||
|
|
||||||
class MeterValue(BaseModel):
|
class MeterValue(BaseModel):
|
||||||
id: str
|
id: UUID
|
||||||
timestamp: datetime
|
timestamp: datetime
|
||||||
measurand: Measurand
|
measurand: Measurand
|
||||||
phase_type: PhaseType
|
phase_type: PhaseType
|
|
@ -1,5 +1,6 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import enum
|
import enum
|
||||||
|
from uuid import UUID
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
class TransactionStatus(enum.Enum):
|
class TransactionStatus(enum.Enum):
|
||||||
|
@ -30,7 +31,7 @@ class TransactionEventTriggerReason(enum.Enum):
|
||||||
RESET_COMMAND = "ResetCommand"
|
RESET_COMMAND = "ResetCommand"
|
||||||
|
|
||||||
class Transaction(BaseModel):
|
class Transaction(BaseModel):
|
||||||
id: str
|
id: UUID
|
||||||
status: TransactionStatus
|
status: TransactionStatus
|
||||||
started_at: datetime
|
started_at: datetime
|
||||||
ended_at: datetime
|
ended_at: datetime
|
|
@ -1,6 +1,7 @@
|
||||||
|
from uuid import UUID
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from schemas.id_token import IdToken
|
from app.schemas.id_token import IdToken
|
||||||
|
|
||||||
class UserBase(BaseModel):
|
class UserBase(BaseModel):
|
||||||
friendly_name: str
|
friendly_name: str
|
||||||
|
@ -10,7 +11,7 @@ class UserCreate(UserBase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class User(UserBase):
|
class User(UserBase):
|
||||||
id: str
|
id: UUID
|
||||||
id_tokens: list[IdToken] = []
|
id_tokens: list[IdToken] = []
|
||||||
|
|
||||||
class Config:
|
class Config:
|
0
app/util/__init__.py
Normal file
0
app/util/__init__.py
Normal file
|
@ -1,9 +1,13 @@
|
||||||
import base64
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
|
from uuid import UUID
|
||||||
from starlette.authentication import (
|
from starlette.authentication import (
|
||||||
AuthCredentials, AuthenticationBackend, AuthenticationError, SimpleUser
|
AuthCredentials, AuthenticationBackend, AuthenticationError, SimpleUser
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from app.database import SessionLocal
|
||||||
|
from app.models.chargepoint import ChargePoint
|
||||||
|
|
||||||
class BasicAuthBackend(AuthenticationBackend):
|
class BasicAuthBackend(AuthenticationBackend):
|
||||||
async def authenticate(self, conn):
|
async def authenticate(self, conn):
|
||||||
if "Authorization" not in conn.headers:
|
if "Authorization" not in conn.headers:
|
||||||
|
@ -19,5 +23,16 @@ class BasicAuthBackend(AuthenticationBackend):
|
||||||
raise AuthenticationError('Invalid basic auth credentials')
|
raise AuthenticationError('Invalid basic auth credentials')
|
||||||
|
|
||||||
username, _, password = decoded.partition(":")
|
username, _, password = decoded.partition(":")
|
||||||
# TODO: You'd want to verify the username and password here.
|
try:
|
||||||
return AuthCredentials(["authenticated"]), SimpleUser(username)
|
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
|
|
@ -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"))
|
|
|
@ -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")
|
|
|
@ -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))
|
|
|
@ -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))
|
|
|
@ -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")
|
|
|
@ -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()
|
|
|
@ -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)
|
|
|
@ -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
|
|
Loading…
Reference in a new issue