Implement transaction handling
This commit is contained in:
parent
161c6aa027
commit
a65dee8962
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
**/__pycache__
|
**/__pycache__
|
||||||
simple-ocpp-cs.db
|
simple-ocpp-cs.db
|
||||||
|
.env
|
22
app/main.py
22
app/main.py
|
@ -1,13 +1,20 @@
|
||||||
from fastapi import FastAPI, Request
|
from dotenv import load_dotenv
|
||||||
from fastapi.exceptions import RequestValidationError
|
from fastapi import FastAPI
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
from starlette.middleware.authentication import AuthenticationMiddleware
|
from starlette.middleware.authentication import AuthenticationMiddleware
|
||||||
import uvicorn
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
from app.database import engine, Base
|
from app.database import engine, Base
|
||||||
from app.models import *
|
from app.models import *
|
||||||
|
|
||||||
from app.routers import chargepoint_v1, id_token_v1, ocpp_v1, user_v1
|
from app.routers import (
|
||||||
|
chargepoint_v1,
|
||||||
|
id_token_v1,
|
||||||
|
meter_value_v1,
|
||||||
|
ocpp_v1,
|
||||||
|
transaction_v1,
|
||||||
|
user_v1
|
||||||
|
)
|
||||||
from app.util.websocket_auth_backend import BasicAuthBackend
|
from app.util.websocket_auth_backend import BasicAuthBackend
|
||||||
|
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
@ -29,11 +36,10 @@ def create_app():
|
||||||
app.include_router(chargepoint_v1.router, prefix="/v1")
|
app.include_router(chargepoint_v1.router, prefix="/v1")
|
||||||
app.include_router(id_token_v1.router, prefix="/v1")
|
app.include_router(id_token_v1.router, prefix="/v1")
|
||||||
app.include_router(user_v1.router, prefix="/v1")
|
app.include_router(user_v1.router, prefix="/v1")
|
||||||
|
app.include_router(meter_value_v1.router, prefix="/v1")
|
||||||
|
app.include_router(transaction_v1.router, prefix="/v1")
|
||||||
app.mount(path="/v1/ocpp", app=create_ocpp_app())
|
app.mount(path="/v1/ocpp", app=create_ocpp_app())
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import uuid
|
import uuid
|
||||||
from sqlalchemy import Uuid, Boolean, Column, DateTime, String
|
from sqlalchemy import ForeignKey, Numeric, Uuid, Boolean, Column, DateTime, String
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from app.database import Base
|
from app.database import Base
|
||||||
|
@ -8,9 +8,10 @@ class ChargePoint(Base):
|
||||||
__tablename__ = "chargepoints"
|
__tablename__ = "chargepoints"
|
||||||
|
|
||||||
id = Column(Uuid, primary_key=True, default=uuid.uuid4)
|
id = Column(Uuid, primary_key=True, default=uuid.uuid4)
|
||||||
friendly_name = Column(String, unique=True, index=True)
|
identity = Column(String, unique=True, index=True)
|
||||||
is_active = Column(Boolean, default=True)
|
is_active = Column(Boolean, default=True)
|
||||||
password = Column(String)
|
password = Column(String)
|
||||||
|
price = Column(Numeric(10, 2))
|
||||||
|
|
||||||
last_seen = Column(DateTime, nullable=True)
|
last_seen = Column(DateTime, nullable=True)
|
||||||
vendor_name = Column(String, nullable=True)
|
vendor_name = Column(String, nullable=True)
|
||||||
|
@ -18,4 +19,7 @@ class ChargePoint(Base):
|
||||||
serial_number = Column(String, nullable=True)
|
serial_number = Column(String, nullable=True)
|
||||||
firmware_version = Column(String, nullable=True)
|
firmware_version = Column(String, nullable=True)
|
||||||
|
|
||||||
|
learn_user_id = Column(Uuid, ForeignKey("users.id"), nullable=True)
|
||||||
|
learn_until = Column(DateTime, nullable=True)
|
||||||
|
|
||||||
connectors = relationship("Connector", cascade="delete, delete-orphan")
|
connectors = relationship("Connector", cascade="delete, delete-orphan")
|
||||||
|
|
|
@ -4,14 +4,14 @@ from sqlalchemy import Uuid, Column, DateTime, Enum, Float, ForeignKey, String
|
||||||
from app.database import Base
|
from app.database import Base
|
||||||
from app.schemas.meter_value import Measurand, PhaseType
|
from app.schemas.meter_value import Measurand, PhaseType
|
||||||
|
|
||||||
class Transaction(Base):
|
class MeterValue(Base):
|
||||||
__tablename__ = "meter_values"
|
__tablename__ = "meter_values"
|
||||||
|
|
||||||
id = Column(Uuid, primary_key=True, default=uuid.uuid4)
|
id = Column(Uuid, primary_key=True, default=uuid.uuid4)
|
||||||
timestamp = Column(DateTime, index=True)
|
timestamp = Column(DateTime, index=True)
|
||||||
measurand = Column(Enum(Measurand))
|
measurand = Column(Enum(Measurand), index=True)
|
||||||
phase_type = Column(Enum(PhaseType))
|
phase_type = Column(Enum(PhaseType), nullable=True)
|
||||||
unit = Column(String)
|
unit = Column(String, nullable=True)
|
||||||
value = Column(Float)
|
value = Column(Float)
|
||||||
|
|
||||||
transaction_id = Column(Uuid, ForeignKey("transactions.id"), index=True)
|
transaction_id = Column(String, ForeignKey("transactions.id"), index=True)
|
|
@ -1,6 +1,4 @@
|
||||||
import uuid
|
from sqlalchemy import String, Uuid, Column, DateTime, Enum, Numeric, ForeignKey
|
||||||
from sqlalchemy import Uuid, Column, DateTime, Enum, Float, ForeignKey
|
|
||||||
from sqlalchemy.orm import relationship, backref
|
|
||||||
|
|
||||||
from app.schemas.transaction import TransactionEventTriggerReason, TransactionStatus
|
from app.schemas.transaction import TransactionEventTriggerReason, TransactionStatus
|
||||||
from app.database import Base
|
from app.database import Base
|
||||||
|
@ -8,13 +6,14 @@ from app.database import Base
|
||||||
class Transaction(Base):
|
class Transaction(Base):
|
||||||
__tablename__ = "transactions"
|
__tablename__ = "transactions"
|
||||||
|
|
||||||
id = Column(Uuid, primary_key=True, default=uuid.uuid4)
|
id = Column(String, primary_key=True)
|
||||||
status = Column(Enum(TransactionStatus), index=True)
|
status = Column(Enum(TransactionStatus), index=True)
|
||||||
started_at = Column(DateTime, index=True)
|
started_at = Column(DateTime, index=True)
|
||||||
ended_at = Column(DateTime, nullable=True, index=True)
|
ended_at = Column(DateTime, nullable=True, index=True)
|
||||||
meter_start = Column(Float)
|
meter_start = Column(Numeric(10,2))
|
||||||
meter_end = Column(Float, nullable=True)
|
meter_end = Column(Numeric(10,2), nullable=True)
|
||||||
end_reason = Column(Enum(TransactionEventTriggerReason))
|
end_reason = Column(Enum(TransactionEventTriggerReason), nullable=True)
|
||||||
|
price = Column(Numeric(10,2))
|
||||||
|
|
||||||
connector_id = Column(Uuid, ForeignKey("connectors.id"), index=True)
|
user_id = Column(Uuid, ForeignKey("users.id"), nullable=True, index=True)
|
||||||
id_token_id = Column(Uuid, ForeignKey("id_tokens.id"), index= True)
|
chargepoint_id = Column(Uuid, ForeignKey("chargepoints.id"), index=True)
|
||||||
|
|
|
@ -12,3 +12,4 @@ class User(Base):
|
||||||
is_active = Column(Boolean, default=True)
|
is_active = Column(Boolean, default=True)
|
||||||
|
|
||||||
id_tokens = relationship("IdToken", back_populates="owner", cascade="delete, delete-orphan")
|
id_tokens = relationship("IdToken", back_populates="owner", cascade="delete, delete-orphan")
|
||||||
|
transactions = relationship("Transaction", cascade="delete, delete-orphan")
|
||||||
|
|
|
@ -1,24 +1,61 @@
|
||||||
from datetime import datetime, UTC
|
from datetime import datetime, UTC
|
||||||
import os
|
import os
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from ocpp.routing import on
|
from ocpp.routing import on
|
||||||
from ocpp.v201 import ChargePoint as cp
|
from ocpp.v201 import ChargePoint as cp
|
||||||
from ocpp.v201 import call_result
|
from ocpp.v201 import call_result
|
||||||
from ocpp.v201.datatypes import IdTokenInfoType, IdTokenType
|
from ocpp.v201.datatypes import IdTokenInfoType, IdTokenType
|
||||||
from ocpp.v201.enums import Action, RegistrationStatusType, AuthorizationStatusType, IdTokenType as IdTokenEnumType
|
from ocpp.v201.enums import Action, RegistrationStatusType, AuthorizationStatusType, IdTokenType as IdTokenEnumType, TransactionEventType
|
||||||
|
|
||||||
from app.database import SessionLocal
|
from app.database import SessionLocal
|
||||||
from app.models.chargepoint import ChargePoint as DbChargePoint
|
from app.models.chargepoint import ChargePoint as DbChargePoint
|
||||||
from app.models.connector import Connector
|
from app.models.connector import Connector as DbConnector
|
||||||
from app.models.id_token import IdToken
|
from app.models.id_token import IdToken as DbIdToken
|
||||||
|
from app.models.transaction import Transaction as DbTransaction
|
||||||
|
from app.models.meter_value import MeterValue as DbMeterValue
|
||||||
from app.schemas.connector import ConnectorStatus
|
from app.schemas.connector import ConnectorStatus
|
||||||
|
from app.schemas.transaction import TransactionStatus, TransactionEventTriggerReason
|
||||||
|
from app.schemas.meter_value import Measurand, PhaseType
|
||||||
|
|
||||||
class ChargePoint(cp):
|
class ChargePoint(cp):
|
||||||
|
|
||||||
|
async def __update_last_seen(self):
|
||||||
|
with SessionLocal() as db:
|
||||||
|
db_chargepoint = db.query(DbChargePoint).filter(DbChargePoint.identity == self.id).first()
|
||||||
|
db_chargepoint.last_seen = datetime.now(UTC)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
async def __get_id_token_info(self, id_token) -> IdTokenInfoType:
|
||||||
|
if id_token["type"] not in ["ISO14443", "ISO15693"]:
|
||||||
|
return IdTokenInfoType(
|
||||||
|
status=AuthorizationStatusType.invalid
|
||||||
|
)
|
||||||
|
|
||||||
|
with SessionLocal() as db:
|
||||||
|
db_id_token = db.query(DbIdToken).filter(DbIdToken.token == id_token["id_token"]).first()
|
||||||
|
if db_id_token == None:
|
||||||
|
return IdTokenInfoType(
|
||||||
|
status=AuthorizationStatusType.unknown
|
||||||
|
)
|
||||||
|
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 id_token_info
|
||||||
|
|
||||||
@on(Action.BootNotification)
|
@on(Action.BootNotification)
|
||||||
async def on_boot_notification(self, charging_station, **kwargs):
|
async def on_boot_notification(self, charging_station, **kwargs):
|
||||||
with SessionLocal() as db:
|
with SessionLocal() as db:
|
||||||
db_chargepoint = db.query(DbChargePoint).filter(DbChargePoint.friendly_name == self.id).first()
|
db_chargepoint = db.query(DbChargePoint).filter(DbChargePoint.identity == self.id).first()
|
||||||
db_chargepoint.last_seen = datetime.now(UTC)
|
db_chargepoint.last_seen = datetime.now(UTC)
|
||||||
for key in charging_station.keys():
|
for key in charging_station.keys():
|
||||||
if key in db_chargepoint.__dict__:
|
if key in db_chargepoint.__dict__:
|
||||||
|
@ -32,10 +69,7 @@ class ChargePoint(cp):
|
||||||
|
|
||||||
@on(Action.Heartbeat)
|
@on(Action.Heartbeat)
|
||||||
async def on_heartbeat_request(self):
|
async def on_heartbeat_request(self):
|
||||||
with SessionLocal() as db:
|
await self.__update_last_seen()
|
||||||
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(
|
return call_result.HeartbeatPayload(
|
||||||
current_time=datetime.now(UTC).isoformat()
|
current_time=datetime.now(UTC).isoformat()
|
||||||
)
|
)
|
||||||
|
@ -43,16 +77,16 @@ class ChargePoint(cp):
|
||||||
@on(Action.StatusNotification)
|
@on(Action.StatusNotification)
|
||||||
async def on_status_notification(self, evse_id: int, connector_id: int, connector_status: str, **kwargs):
|
async def on_status_notification(self, evse_id: int, connector_id: int, connector_status: str, **kwargs):
|
||||||
with SessionLocal() as db:
|
with SessionLocal() as db:
|
||||||
db_chargepoint = db.query(DbChargePoint).filter(DbChargePoint.friendly_name == self.id).first()
|
db_chargepoint = db.query(DbChargePoint).filter(DbChargePoint.identity == self.id).first()
|
||||||
db_chargepoint.last_seen = datetime.now(UTC)
|
db_chargepoint.last_seen = datetime.now(UTC)
|
||||||
|
|
||||||
db_connector = db.query(Connector).filter(
|
db_connector = db.query(DbConnector).filter(
|
||||||
Connector.chargepoint_id == db_chargepoint.id,
|
DbConnector.chargepoint_id == db_chargepoint.id,
|
||||||
Connector.evse == evse_id,
|
DbConnector.evse == evse_id,
|
||||||
Connector.index == connector_id
|
DbConnector.index == connector_id
|
||||||
).first()
|
).first()
|
||||||
if db_connector == None:
|
if db_connector == None:
|
||||||
db_connector = Connector(
|
db_connector = DbConnector(
|
||||||
chargepoint_id = db_chargepoint.id,
|
chargepoint_id = db_chargepoint.id,
|
||||||
evse = evse_id,
|
evse = evse_id,
|
||||||
index = connector_id,
|
index = connector_id,
|
||||||
|
@ -68,39 +102,95 @@ class ChargePoint(cp):
|
||||||
|
|
||||||
@on(Action.Authorize)
|
@on(Action.Authorize)
|
||||||
async def on_authorize(self, id_token, **kwargs):
|
async def on_authorize(self, id_token, **kwargs):
|
||||||
if id_token["type"] not in ["ISO14443", "ISO15693"]:
|
await self.__update_last_seen()
|
||||||
return call_result.AuthorizePayload(
|
id_token_info = await self.__get_id_token_info(id_token)
|
||||||
id_token_info=IdTokenInfoType(
|
return call_result.AuthorizePayload(id_token_info)
|
||||||
status=AuthorizationStatusType.invalid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
with SessionLocal() as db:
|
|
||||||
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_token"]).first()
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
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)
|
@on(Action.TransactionEvent)
|
||||||
async def on_transaction_event(self):
|
async def on_transaction_event(
|
||||||
|
self,
|
||||||
|
event_type,
|
||||||
|
timestamp,
|
||||||
|
trigger_reason,
|
||||||
|
transaction_info,
|
||||||
|
id_token,
|
||||||
|
meter_value,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
if id_token != None:
|
||||||
|
id_token_info = await self.__get_id_token_info(id_token)
|
||||||
|
|
||||||
|
with SessionLocal() as db:
|
||||||
|
chargepoint = db.query(DbChargePoint).filter(DbChargePoint.identity == self.id).first()
|
||||||
|
chargepoint.last_seen = datetime.now(UTC)
|
||||||
|
|
||||||
|
if event_type == str(TransactionEventType.started):
|
||||||
|
meter_start=0
|
||||||
|
if meter_value != None:
|
||||||
|
for meter_value_entry in meter_value:
|
||||||
|
for sampled_value in meter_value_entry['sampled_value']:
|
||||||
|
if "measurand" in sampled_value.keys():
|
||||||
|
if sampled_value['measurand'] == str(Measurand.ENERGY_ACTIVE_IMPORT_REGISTER):
|
||||||
|
meter_start = sampled_value['value']
|
||||||
|
else:
|
||||||
|
meter_start = sampled_value['value']
|
||||||
|
transaction = DbTransaction(
|
||||||
|
id=transaction_info["transaction_id"],
|
||||||
|
status=TransactionStatus.ONGOING,
|
||||||
|
started_at=datetime.fromisoformat(timestamp),
|
||||||
|
meter_start=meter_start,
|
||||||
|
price=chargepoint.price,
|
||||||
|
chargepoint_id=chargepoint.id
|
||||||
|
)
|
||||||
|
if id_token != None:
|
||||||
|
if id_token_info.status == AuthorizationStatusType.accepted:
|
||||||
|
transaction.user_id = UUID(id_token_info.group_id_token.id_token)
|
||||||
|
db.add(transaction)
|
||||||
|
elif event_type == str(TransactionEventType.updated):
|
||||||
|
transaction = db.get(DbTransaction, transaction_info["transaction_id"])
|
||||||
|
if meter_value != None:
|
||||||
|
for meter_value_entry in meter_value:
|
||||||
|
timestamp = datetime.fromisoformat(meter_value_entry['timestamp'])
|
||||||
|
for sampled_value in meter_value_entry['sampled_value']:
|
||||||
|
db_meter_value = DbMeterValue()
|
||||||
|
db_meter_value.transaction_id = transaction.id
|
||||||
|
db_meter_value.timestamp = timestamp
|
||||||
|
if "measurand" in sampled_value.keys():
|
||||||
|
db_meter_value.measurand = Measurand(sampled_value['measurand'])
|
||||||
|
else:
|
||||||
|
db_meter_value.measurand = Measurand.ENERGY_ACTIVE_IMPORT_REGISTER
|
||||||
|
if "phase" in sampled_value.keys():
|
||||||
|
db_meter_value.phase_type = PhaseType(sampled_value['phase'])
|
||||||
|
if "unit_of_measure" in sampled_value.keys():
|
||||||
|
if "unit" in sampled_value['unit_of_measure']:
|
||||||
|
db_meter_value.unit = sampled_value['unit_of_measure']['unit']
|
||||||
|
else:
|
||||||
|
db_meter_value.unit = "Wh"
|
||||||
|
db_meter_value.value = sampled_value['value']
|
||||||
|
db.add(db_meter_value)
|
||||||
|
if id_token != None:
|
||||||
|
if id_token_info.status == AuthorizationStatusType.accepted:
|
||||||
|
transaction.user_id = UUID(id_token_info.group_id_token.id_token)
|
||||||
|
elif event_type == str(TransactionEventType.ended):
|
||||||
|
transaction = db.get(DbTransaction, transaction_info["transaction_id"])
|
||||||
|
transaction.status = TransactionStatus.ENDED
|
||||||
|
transaction.ended_at = datetime.fromisoformat(timestamp)
|
||||||
|
transaction.end_reason = TransactionEventTriggerReason(trigger_reason)
|
||||||
|
meter_end=0
|
||||||
|
if meter_value != None:
|
||||||
|
for meter_value_entry in meter_value:
|
||||||
|
for sampled_value in meter_value_entry['sampled_value']:
|
||||||
|
if "measurand" in sampled_value.keys():
|
||||||
|
if sampled_value['measurand'] == str(Measurand.ENERGY_ACTIVE_IMPORT_REGISTER):
|
||||||
|
meter_end = sampled_value['value']
|
||||||
|
else:
|
||||||
|
meter_end = sampled_value['value']
|
||||||
|
transaction.meter_end = meter_end
|
||||||
|
if id_token != None:
|
||||||
|
if id_token_info.status == AuthorizationStatusType.accepted:
|
||||||
|
transaction.user_id = UUID(id_token_info.group_id_token.id_token)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
if id_token != None:
|
||||||
|
return call_result.TransactionEventPayload(id_token_info=id_token_info)
|
||||||
return call_result.TransactionEventPayload()
|
return call_result.TransactionEventPayload()
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
|
from datetime import datetime, timedelta, UTC
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from fastapi import APIRouter, HTTPException, Security
|
from fastapi import APIRouter, HTTPException, Security
|
||||||
from fastapi.params import Depends
|
from fastapi.params import Depends
|
||||||
|
@ -18,7 +19,9 @@ from app.schemas.chargepoint import (
|
||||||
ChargePointResetRequest,
|
ChargePointResetRequest,
|
||||||
ChargePointResetResponse
|
ChargePointResetResponse
|
||||||
)
|
)
|
||||||
|
from app.schemas.id_token import IdTokenLearnRequest, IdTokenLearnResponse
|
||||||
from app.models.chargepoint import ChargePoint as DbChargePoint
|
from app.models.chargepoint import ChargePoint as DbChargePoint
|
||||||
|
from app.models.user import User as DbUser
|
||||||
from app.security import get_api_key
|
from app.security import get_api_key
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
|
@ -77,9 +80,10 @@ async def create_chargepoint(
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
chargepoint_db = DbChargePoint(
|
chargepoint_db = DbChargePoint(
|
||||||
friendly_name=chargepoint.friendly_name,
|
identity=chargepoint.identity,
|
||||||
is_active=chargepoint.is_active,
|
is_active=chargepoint.is_active,
|
||||||
password=''.join(random.choice(string.ascii_letters + string.digits) for i in range(24))
|
password=''.join(random.choice(string.ascii_letters + string.digits) for i in range(24)),
|
||||||
|
price=chargepoint.price
|
||||||
)
|
)
|
||||||
db.add(chargepoint_db)
|
db.add(chargepoint_db)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
@ -139,3 +143,73 @@ async def reset_chargepoint(
|
||||||
return ChargePointResetResponse(status=response.status)
|
return ChargePointResetResponse(status=response.status)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
raise HTTPException(status_code=503, detail="Chargepoint didn't respond in time.")
|
raise HTTPException(status_code=503, detail="Chargepoint didn't respond in time.")
|
||||||
|
|
||||||
|
@router.post(path="/{chargepoint_id}/token-learning", status_code=201, response_model=IdTokenLearnResponse)
|
||||||
|
async def create_id_token_learn_request(
|
||||||
|
chargepoint_id: UUID,
|
||||||
|
learn_request: IdTokenLearnRequest,
|
||||||
|
api_key: str = Security(get_api_key),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
chargepoint = db.get(DbChargePoint, chargepoint_id)
|
||||||
|
if chargepoint is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Chargepoint not found")
|
||||||
|
|
||||||
|
owner = db.get(DbUser, learn_request.user_id)
|
||||||
|
if owner == None:
|
||||||
|
raise HTTPException(status_code=422, detail=[{
|
||||||
|
"loc": ["body", "user_id"],
|
||||||
|
"msg": "Target user not found",
|
||||||
|
"type": "invalid_relation"
|
||||||
|
}])
|
||||||
|
|
||||||
|
chargepoint.learn_user_id = learn_request.user_id
|
||||||
|
|
||||||
|
if learn_request.until == None:
|
||||||
|
chargepoint.learn_until = datetime.now(UTC) + timedelta(minutes=5)
|
||||||
|
else:
|
||||||
|
chargepoint.learn_until = learn_request.until
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return IdTokenLearnResponse(
|
||||||
|
user_id=chargepoint.learn_user_id,
|
||||||
|
until=chargepoint.learn_until
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get(path="/{chargepoint_id}/token-learning", response_model=IdTokenLearnResponse)
|
||||||
|
async def get_id_token_learn_request(
|
||||||
|
chargepoint_id: UUID,
|
||||||
|
api_key: str = Security(get_api_key),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
chargepoint = db.get(DbChargePoint, chargepoint_id)
|
||||||
|
if chargepoint is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Chargepoint not found")
|
||||||
|
|
||||||
|
if chargepoint.learn_user_id == None:
|
||||||
|
raise HTTPException(status_code=404, detail="No active learning request")
|
||||||
|
|
||||||
|
return IdTokenLearnResponse(
|
||||||
|
user_id=chargepoint.learn_user_id,
|
||||||
|
until=chargepoint.learn_until
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.delete(path="/{chargepoint_id}/token-learning", response_model=[])
|
||||||
|
async def get_id_token_learn_request(
|
||||||
|
chargepoint_id: UUID,
|
||||||
|
api_key: str = Security(get_api_key),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
chargepoint = db.get(DbChargePoint, chargepoint_id)
|
||||||
|
if chargepoint is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Chargepoint not found")
|
||||||
|
|
||||||
|
if chargepoint.learn_user_id == None:
|
||||||
|
raise HTTPException(status_code=404, detail="No active learning request")
|
||||||
|
|
||||||
|
chargepoint.learn_user_id = None
|
||||||
|
chargepoint.learn_until = None
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
22
app/routers/meter_value_v1.py
Normal file
22
app/routers/meter_value_v1.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from fastapi.params import Security
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.security import get_api_key
|
||||||
|
from app.database import get_db
|
||||||
|
from app.schemas.meter_value import MeterValue
|
||||||
|
from app.models.meter_value import MeterValue as DbMeterValue
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/meter-values",
|
||||||
|
tags=["MeterValue (v1)"]
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get(path="", response_model=list[MeterValue])
|
||||||
|
async def get_meter_values(
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 20,
|
||||||
|
api_key: str = Security(get_api_key),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
return db.query(DbMeterValue).offset(skip).limit(limit).all()
|
|
@ -7,19 +7,19 @@ from app.util.websocket_wrapper import WebSocketWrapper
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.websocket("/{charging_station_friendly_name}")
|
@router.websocket("/{chargepoint_identity}")
|
||||||
async def websocket_endpoint(
|
async def websocket_endpoint(
|
||||||
*,
|
*,
|
||||||
websocket: WebSocket,
|
websocket: WebSocket,
|
||||||
charging_station_friendly_name: str,
|
chargepoint_identity: 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.friendly_name != charging_station_friendly_name):
|
if (websocket.user.identity != chargepoint_identity):
|
||||||
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' (%s) connected", charging_station_friendly_name, websocket.user.id)
|
logging.info("Charging station '%s' (%s) connected", chargepoint_identity, websocket.user.id)
|
||||||
|
|
||||||
# Check protocols
|
# Check protocols
|
||||||
try:
|
try:
|
||||||
|
@ -43,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_friendly_name, WebSocketWrapper(websocket))
|
cp = ChargePoint(chargepoint_identity, WebSocketWrapper(websocket))
|
||||||
await chargepoint_manager.start(websocket.user.id, cp)
|
await chargepoint_manager.start(websocket.user.id, cp)
|
||||||
|
|
22
app/routers/transaction_v1.py
Normal file
22
app/routers/transaction_v1.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from fastapi.params import Security
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.security import get_api_key
|
||||||
|
from app.database import get_db
|
||||||
|
from app.schemas.transaction import Transaction
|
||||||
|
from app.models.transaction import Transaction as DbTransaction
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/transactions",
|
||||||
|
tags=["Transaction (v1)"]
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get(path="", response_model=list[Transaction])
|
||||||
|
async def get_transactions(
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 20,
|
||||||
|
api_key: str = Security(get_api_key),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
return db.query(DbTransaction).offset(skip).limit(limit).all()
|
|
@ -1,4 +1,5 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
@ -8,12 +9,14 @@ from app.schemas.connector import Connector
|
||||||
from ocpp.v201.enums import ResetType, ResetStatusType
|
from ocpp.v201.enums import ResetType, ResetStatusType
|
||||||
|
|
||||||
class ChargePointBase(BaseModel):
|
class ChargePointBase(BaseModel):
|
||||||
friendly_name: str
|
identity: str
|
||||||
is_active: bool
|
is_active: bool
|
||||||
|
price: Decimal
|
||||||
|
|
||||||
class ChargePointUpdate(BaseModel):
|
class ChargePointUpdate(BaseModel):
|
||||||
friendly_name: Optional[str] = None
|
identity: Optional[str] = None
|
||||||
is_active: Optional[bool] = None
|
is_active: Optional[bool] = None
|
||||||
|
price: Optional[Decimal]= None
|
||||||
|
|
||||||
class ChargePointCreate(ChargePointBase):
|
class ChargePointCreate(ChargePointBase):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
@ -23,3 +24,12 @@ class IdToken(IdTokenBase):
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
||||||
|
class IdTokenLearnBase(BaseModel):
|
||||||
|
user_id: UUID
|
||||||
|
|
||||||
|
class IdTokenLearnRequest(IdTokenLearnBase):
|
||||||
|
until: Optional[datetime] = None
|
||||||
|
|
||||||
|
class IdTokenLearnResponse(IdTokenLearnBase):
|
||||||
|
until: datetime
|
|
@ -1,5 +1,7 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
import enum
|
import enum
|
||||||
|
from typing import Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
@ -46,9 +48,9 @@ class MeterValue(BaseModel):
|
||||||
id: UUID
|
id: UUID
|
||||||
timestamp: datetime
|
timestamp: datetime
|
||||||
measurand: Measurand
|
measurand: Measurand
|
||||||
phase_type: PhaseType
|
phase_type: Optional[PhaseType] = None
|
||||||
unit: str
|
unit: Optional[str] = None
|
||||||
value: float
|
value: Decimal
|
||||||
transaction_id: str
|
transaction_id: str
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
import enum
|
import enum
|
||||||
|
from typing import Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
@ -31,15 +33,16 @@ class TransactionEventTriggerReason(enum.Enum):
|
||||||
RESET_COMMAND = "ResetCommand"
|
RESET_COMMAND = "ResetCommand"
|
||||||
|
|
||||||
class Transaction(BaseModel):
|
class Transaction(BaseModel):
|
||||||
id: UUID
|
id: str
|
||||||
status: TransactionStatus
|
status: TransactionStatus
|
||||||
started_at: datetime
|
started_at: datetime
|
||||||
ended_at: datetime
|
ended_at: Optional[datetime] = None
|
||||||
meter_start: float
|
meter_start: Decimal
|
||||||
meter_end: float
|
meter_end: Optional[Decimal] = None
|
||||||
end_reason: TransactionEventTriggerReason
|
end_reason: Optional[TransactionEventTriggerReason] = None
|
||||||
connector_id: str
|
price: Decimal
|
||||||
id_token_id: str
|
user_id: Optional[UUID] = None
|
||||||
|
chargepoint_id: UUID
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
|
@ -1,7 +1,7 @@
|
||||||
import base64
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
from starlette.authentication import (
|
from starlette.authentication import (
|
||||||
AuthCredentials, AuthenticationBackend, AuthenticationError, SimpleUser
|
AuthCredentials, AuthenticationBackend, AuthenticationError
|
||||||
)
|
)
|
||||||
|
|
||||||
from app.database import SessionLocal
|
from app.database import SessionLocal
|
||||||
|
@ -24,7 +24,7 @@ class BasicAuthBackend(AuthenticationBackend):
|
||||||
username, _, password = decoded.partition(":")
|
username, _, password = decoded.partition(":")
|
||||||
|
|
||||||
with SessionLocal() as db:
|
with SessionLocal() as db:
|
||||||
chargepoint = db.query(ChargePoint).filter(ChargePoint.friendly_name == username).first()
|
chargepoint = db.query(ChargePoint).filter(ChargePoint.identity == username).first()
|
||||||
if chargepoint is None:
|
if chargepoint is None:
|
||||||
raise AuthenticationError('Invalid basic auth credentials')
|
raise AuthenticationError('Invalid basic auth credentials')
|
||||||
if chargepoint.password != password:
|
if chargepoint.password != password:
|
||||||
|
|
Loading…
Reference in a new issue