diff --git a/.gitignore b/.gitignore index 9a5ab14..3b96008 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +**/__init__.py **/__pycache__ simple-ocpp-cs.db \ No newline at end of file diff --git a/alembic.ini b/alembic.ini deleted file mode 100644 index 3a46a67..0000000 --- a/alembic.ini +++ /dev/null @@ -1,113 +0,0 @@ -# A generic, single database configuration. - -[alembic] -# path to migration scripts -script_location = alembic - -# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s -# Uncomment the line below if you want the files to be prepended with date and time -# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file -# for all available tokens -file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s-%%(slug)s - -# sys.path path, will be prepended to sys.path if present. -# defaults to the current working directory. -prepend_sys_path = . - -# timezone to use when rendering the date within the migration file -# as well as the filename. -# If specified, requires the python>=3.9 or backports.zoneinfo library. -# Any required deps can installed by adding `alembic[tz]` to the pip requirements -# string value is passed to ZoneInfo() -# leave blank for localtime -timezone = UTC - -# max length of characters to apply to the -# "slug" field -# truncate_slug_length = 40 - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - -# set to 'true' to allow .pyc and .pyo files without -# a source .py file to be detected as revisions in the -# versions/ directory -# sourceless = false - -# version location specification; This defaults -# to alembic/versions. When using multiple version -# directories, initial revisions must be specified with --version-path. -# The path separator used here should be the separator specified by "version_path_separator" below. -# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions - -# version path separator; As mentioned above, this is the character used to split -# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. -# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. -# Valid values for version_path_separator are: -# -# version_path_separator = : -# version_path_separator = ; -# version_path_separator = space -version_path_separator = os # Use os.pathsep. Default configuration used for new projects. - -# set to 'true' to search source files recursively -# in each "version_locations" directory -# new in Alembic version 1.10 -# recursive_version_locations = false - -# the output encoding used when revision files -# are written from script.py.mako -output_encoding = utf-8 - -[post_write_hooks] -# post_write_hooks defines scripts or Python functions that are run -# on newly generated revision scripts. See the documentation for further -# detail and examples - -# format using "black" - use the console_scripts runner, against the "black" entrypoint -# hooks = black -# black.type = console_scripts -# black.entrypoint = black -# black.options = -l 79 REVISION_SCRIPT_FILENAME - -# lint with attempts to fix using "ruff" - use the exec runner, execute a binary -# hooks = ruff -# ruff.type = exec -# ruff.executable = %(here)s/.venv/bin/ruff -# ruff.options = --fix REVISION_SCRIPT_FILENAME - -# Logging configuration -[loggers] -keys = root,sqlalchemy,alembic - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S diff --git a/alembic/env.py b/alembic/env.py deleted file mode 100644 index 8d40e79..0000000 --- a/alembic/env.py +++ /dev/null @@ -1,59 +0,0 @@ -from logging.config import fileConfig -import os - -from sqlalchemy import create_engine -from alembic import context - -# Import models for autogenerate support -from app.database import Base -from app.models import * - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -if config.config_file_name is not None: - fileConfig(config.config_file_name) - -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -target_metadata = Base.metadata - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - -def run_migrations_online() -> None: - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - SQLALCHEMY_DATABASE_URL = os.getenv("CS_DATABASE_URL", "sqlite:///./simple-ocpp-cs.db") - - if SQLALCHEMY_DATABASE_URL.startswith("sqlite"): - connectable = create_engine( - SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} - ) - else: - connectable = create_engine(SQLALCHEMY_DATABASE_URL) - - with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - raise NotImplementedError("Offline migration is not supported") -else: - run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako deleted file mode 100644 index fbc4b07..0000000 --- a/alembic/script.py.mako +++ /dev/null @@ -1,26 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision: str = ${repr(up_revision)} -down_revision: Union[str, None] = ${repr(down_revision)} -branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} -depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} - - -def upgrade() -> None: - ${upgrades if upgrades else "pass"} - - -def downgrade() -> None: - ${downgrades if downgrades else "pass"} diff --git a/app/__init__.py b/app/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/models/__init__.py b/app/models/__init__.py deleted file mode 100644 index 0f3f4ba..0000000 --- a/app/models/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -__all__ = [ - "chargepoint", - "connector", - "id_token", - "meter_value", - "transaction", - "user" -] \ No newline at end of file diff --git a/app/models/connector.py b/app/models/connector.py deleted file mode 100644 index 97f168a..0000000 --- a/app/models/connector.py +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 61651b2..0000000 --- a/app/models/id_token.py +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 9cd0835..0000000 --- a/app/models/meter_value.py +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 18cc83c..0000000 --- a/app/models/transaction.py +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 339e5f7..0000000 --- a/app/models/user.py +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index e69de29..0000000 diff --git a/app/ocpp_proto/chargepoint.py b/app/ocpp_proto/chargepoint.py deleted file mode 100644 index d2accb2..0000000 --- a/app/ocpp_proto/chargepoint.py +++ /dev/null @@ -1,88 +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 - -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/app/routers/__init__.py b/app/routers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routers/chargepoint_v1.py b/app/routers/chargepoint_v1.py deleted file mode 100644 index cd768b5..0000000 --- a/app/routers/chargepoint_v1.py +++ /dev/null @@ -1,114 +0,0 @@ -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/app/schemas/__init__.py b/app/schemas/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/schemas/chargepoint.py b/app/schemas/chargepoint.py deleted file mode 100644 index acb30b3..0000000 --- a/app/schemas/chargepoint.py +++ /dev/null @@ -1,31 +0,0 @@ -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/app/util/__init__.py b/app/util/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/poetry.lock b/poetry.lock index 07231f9..23614ad 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,24 +1,5 @@ # This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. -[[package]] -name = "alembic" -version = "1.13.1" -description = "A database migration tool for SQLAlchemy." -optional = false -python-versions = ">=3.8" -files = [ - {file = "alembic-1.13.1-py3-none-any.whl", hash = "sha256:2edcc97bed0bd3272611ce3a98d98279e9c209e7186e43e75bbb1b2bdfdbcc43"}, - {file = "alembic-1.13.1.tar.gz", hash = "sha256:4932c8558bf68f2ee92b9bbcb8218671c627064d5b08939437af6d77dc05e595"}, -] - -[package.dependencies] -Mako = "*" -SQLAlchemy = ">=1.3.0" -typing-extensions = ">=4" - -[package.extras] -tz = ["backports.zoneinfo"] - [[package]] name = "annotated-types" version = "0.6.0" @@ -289,94 +270,6 @@ files = [ [package.dependencies] referencing = ">=0.31.0" -[[package]] -name = "mako" -version = "1.3.3" -description = "A super-fast templating language that borrows the best ideas from the existing templating languages." -optional = false -python-versions = ">=3.8" -files = [ - {file = "Mako-1.3.3-py3-none-any.whl", hash = "sha256:5324b88089a8978bf76d1629774fcc2f1c07b82acdf00f4c5dd8ceadfffc4b40"}, - {file = "Mako-1.3.3.tar.gz", hash = "sha256:e16c01d9ab9c11f7290eef1cfefc093fb5a45ee4a3da09e2fec2e4d1bae54e73"}, -] - -[package.dependencies] -MarkupSafe = ">=0.9.2" - -[package.extras] -babel = ["Babel"] -lingua = ["lingua"] -testing = ["pytest"] - -[[package]] -name = "markupsafe" -version = "2.1.5" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, -] - [[package]] name = "ocpp" version = "0.26.0" @@ -1064,4 +957,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "2b9d4a1a7ceaa46c78814156552aea406a0683f55a574d198d8a65917b476e40" +content-hash = "599bf05928701049ff4348ad877ab3413c9864c2a5bf41ae047c7bbe0704270f" diff --git a/pyproject.toml b/pyproject.toml index c4ccb64..d73594f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ fastapi = "^0.110.0" uvicorn = {extras = ["standard"], version = "^0.28.0"} websockets = "^12.0" sqlalchemy = "^2.0.28" -alembic = "^1.13.1" [build-system] requires = ["poetry-core"] diff --git a/app/database.py b/src/database.py similarity index 81% rename from app/database.py rename to src/database.py index 233eeb5..de3e000 100644 --- a/app/database.py +++ b/src/database.py @@ -14,11 +14,4 @@ else: SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) -Base = declarative_base() - -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() +Base = declarative_base() \ No newline at end of file diff --git a/app/main.py b/src/main.py similarity index 81% rename from app/main.py rename to src/main.py index b9cbb02..ee9aaca 100644 --- a/app/main.py +++ b/src/main.py @@ -2,11 +2,9 @@ from fastapi import FastAPI from starlette.middleware.authentication import AuthenticationMiddleware import uvicorn -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 +from routers import chargepoint_v1, ocpp_v1 +from database import engine, Base +from util.websocket_auth_backend import BasicAuthBackend Base.metadata.create_all(bind=engine) diff --git a/app/models/chargepoint.py b/src/models/chargepoint.py similarity index 54% rename from app/models/chargepoint.py rename to src/models/chargepoint.py index 3c5db63..8115257 100644 --- a/app/models/chargepoint.py +++ b/src/models/chargepoint.py @@ -1,16 +1,15 @@ -import uuid -from sqlalchemy import Uuid, Boolean, Column, DateTime, String +from sqlalchemy import Boolean, Column, DateTime, String from sqlalchemy.orm import relationship -from app.database import Base +from database import Base class ChargePoint(Base): __tablename__ = "chargepoints" - id = Column(Uuid, primary_key=True, default=uuid.uuid4) + id = Column(String, primary_key=True) 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", cascade="delete, delete-orphan") + connectors = relationship("Connector") diff --git a/src/models/connector.py b/src/models/connector.py new file mode 100644 index 0000000..436faf2 --- /dev/null +++ b/src/models/connector.py @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..32ead9d --- /dev/null +++ b/src/models/id_token.py @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..3fe9ff4 --- /dev/null +++ b/src/models/meter_value.py @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000..d7ef02c --- /dev/null +++ b/src/models/transaction.py @@ -0,0 +1,19 @@ +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 new file mode 100644 index 0000000..8925954 --- /dev/null +++ b/src/models/user.py @@ -0,0 +1,12 @@ +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 new file mode 100644 index 0000000..bcdda6a --- /dev/null +++ b/src/ocpp_proto/chargepoint.py @@ -0,0 +1,33 @@ +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/app/ocpp_proto/chargepoint_manager.py b/src/ocpp_proto/chargepoint_manager.py similarity index 56% rename from app/ocpp_proto/chargepoint_manager.py rename to src/ocpp_proto/chargepoint_manager.py index 60ebdc6..5035adb 100644 --- a/app/ocpp_proto/chargepoint_manager.py +++ b/src/ocpp_proto/chargepoint_manager.py @@ -1,23 +1,22 @@ import logging from typing import Any, Coroutine, Dict -from uuid import UUID from websockets import ConnectionClosed -from app.ocpp_proto.chargepoint import ChargePoint +from ocpp_proto.chargepoint import ChargePoint -__active_connections: Dict[UUID, ChargePoint] = {} +__active_connections: Dict[str, ChargePoint] = {} -async def start(id: UUID, cp: ChargePoint): +async def start(cp: ChargePoint): try: - __active_connections[id] = cp + __active_connections[cp.id] = cp await cp.start() except ConnectionClosed: - logging.info("Charging station '%s' (%s) disconnected", cp.id, id) - __active_connections.pop(id, None) + logging.info("Charging station '%s' disconnected", cp.id) + __active_connections.pop(cp.id, None) async def call( - chargepoint_id: UUID, + chargepoint_id: str, payload: Any, suppress: bool = True, unique_id: Any | None = None @@ -28,5 +27,5 @@ async def call( except KeyError as e: raise e -def is_connected(chargepoint_id: UUID): +def is_connected(chargepoint_id: str): return chargepoint_id in __active_connections.keys() diff --git a/src/routers/chargepoint_v1.py b/src/routers/chargepoint_v1.py new file mode 100644 index 0000000..c9ff823 --- /dev/null +++ b/src/routers/chargepoint_v1.py @@ -0,0 +1,45 @@ +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/app/routers/ocpp_v1.py b/src/routers/ocpp_v1.py similarity index 69% rename from app/routers/ocpp_v1.py rename to src/routers/ocpp_v1.py index 3b8ca41..91e122a 100644 --- a/app/routers/ocpp_v1.py +++ b/src/routers/ocpp_v1.py @@ -1,25 +1,24 @@ import logging from fastapi import APIRouter, WebSocket, WebSocketException - -from app.ocpp_proto import chargepoint_manager -from app.ocpp_proto.chargepoint import ChargePoint -from app.util.websocket_wrapper import WebSocketWrapper +from ocpp_proto import chargepoint_manager +from ocpp_proto.chargepoint import ChargePoint +from util.websocket_wrapper import WebSocketWrapper router = APIRouter() -@router.websocket("/{charging_station_friendly_name}") +@router.websocket("/{charging_station_id}") async def websocket_endpoint( *, websocket: WebSocket, - charging_station_friendly_name: str, + charging_station_id: str, ): """ For every new charging station that connects, create a ChargePoint instance and start listening for messages. """ - if (websocket.user.friendly_name != charging_station_friendly_name): + if (websocket.user.username != charging_station_id): 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' connected", charging_station_id) # Check protocols try: @@ -43,5 +42,5 @@ async def websocket_endpoint( # Accept connection and begin communication await websocket.accept(subprotocol="ocpp2.0.1") - cp = ChargePoint(charging_station_friendly_name, WebSocketWrapper(websocket)) - await chargepoint_manager.start(websocket.user.id, cp) + cp = ChargePoint(charging_station_id, WebSocketWrapper(websocket)) + await chargepoint_manager.start(cp) diff --git a/src/schemas/chargepoint.py b/src/schemas/chargepoint.py new file mode 100644 index 0000000..5c4aaad --- /dev/null +++ b/src/schemas/chargepoint.py @@ -0,0 +1,20 @@ +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 diff --git a/app/schemas/connector.py b/src/schemas/connector.py similarity index 90% rename from app/schemas/connector.py rename to src/schemas/connector.py index 786f9e7..41a5645 100644 --- a/app/schemas/connector.py +++ b/src/schemas/connector.py @@ -1,5 +1,4 @@ import enum -from uuid import UUID from pydantic import BaseModel class ConnectorStatus(enum.Enum): @@ -10,7 +9,7 @@ class ConnectorStatus(enum.Enum): FAULTED = "Faulted" class Connector(BaseModel): - id: UUID + id: str evse: int index: int status: ConnectorStatus diff --git a/app/schemas/id_token.py b/src/schemas/id_token.py similarity index 77% rename from app/schemas/id_token.py rename to src/schemas/id_token.py index d279913..2730a68 100644 --- a/app/schemas/id_token.py +++ b/src/schemas/id_token.py @@ -1,7 +1,6 @@ -from uuid import UUID from pydantic import BaseModel -from app.schemas.user import User +from schemas.user import User class IdTokenBase(BaseModel): title: str @@ -11,7 +10,7 @@ class IdTokenCreate(IdTokenBase): pass class IdToken(IdTokenBase): - id: UUID + id: str owner: User class Config: diff --git a/app/schemas/meter_value.py b/src/schemas/meter_value.py similarity index 98% rename from app/schemas/meter_value.py rename to src/schemas/meter_value.py index 5e31278..830180f 100644 --- a/app/schemas/meter_value.py +++ b/src/schemas/meter_value.py @@ -1,6 +1,5 @@ from datetime import datetime import enum -from uuid import UUID from pydantic import BaseModel class PhaseType(enum.Enum): @@ -43,7 +42,7 @@ class Measurand(enum.Enum): VOLTAGE = "Voltage" class MeterValue(BaseModel): - id: UUID + id: str timestamp: datetime measurand: Measurand phase_type: PhaseType diff --git a/app/schemas/transaction.py b/src/schemas/transaction.py similarity index 97% rename from app/schemas/transaction.py rename to src/schemas/transaction.py index 6937be7..7723f6e 100644 --- a/app/schemas/transaction.py +++ b/src/schemas/transaction.py @@ -1,6 +1,5 @@ from datetime import datetime import enum -from uuid import UUID from pydantic import BaseModel class TransactionStatus(enum.Enum): @@ -31,7 +30,7 @@ class TransactionEventTriggerReason(enum.Enum): RESET_COMMAND = "ResetCommand" class Transaction(BaseModel): - id: UUID + id: str status: TransactionStatus started_at: datetime ended_at: datetime diff --git a/app/schemas/user.py b/src/schemas/user.py similarity index 76% rename from app/schemas/user.py rename to src/schemas/user.py index c06817b..eceedfe 100644 --- a/app/schemas/user.py +++ b/src/schemas/user.py @@ -1,7 +1,6 @@ -from uuid import UUID from pydantic import BaseModel -from app.schemas.id_token import IdToken +from schemas.id_token import IdToken class UserBase(BaseModel): friendly_name: str @@ -11,7 +10,7 @@ class UserCreate(UserBase): pass class User(UserBase): - id: UUID + id: str id_tokens: list[IdToken] = [] class Config: diff --git a/app/security.py b/src/security.py similarity index 100% rename from app/security.py rename to src/security.py diff --git a/app/util/websocket_auth_backend.py b/src/util/websocket_auth_backend.py similarity index 54% rename from app/util/websocket_auth_backend.py rename to src/util/websocket_auth_backend.py index a3260a6..12802ea 100644 --- a/app/util/websocket_auth_backend.py +++ b/src/util/websocket_auth_backend.py @@ -1,13 +1,9 @@ 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: @@ -23,16 +19,5 @@ class BasicAuthBackend(AuthenticationBackend): raise AuthenticationError('Invalid basic auth credentials') username, _, password = decoded.partition(":") - 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 + # TODO: You'd want to verify the username and password here. + return AuthCredentials(["authenticated"]), SimpleUser(username) \ No newline at end of file diff --git a/app/util/websocket_wrapper.py b/src/util/websocket_wrapper.py similarity index 100% rename from app/util/websocket_wrapper.py rename to src/util/websocket_wrapper.py