Compare commits
2 commits
b8216f6ade
...
599a60c52b
Author | SHA1 | Date | |
---|---|---|---|
Oliver Traber | 599a60c52b | ||
Oliver Traber | 7740be8bb5 |
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,2 @@
|
||||||
**/__init__.py
|
|
||||||
**/__pycache__
|
**/__pycache__
|
||||||
simple-ocpp-cs.db
|
simple-ocpp-cs.db
|
113
alembic.ini
Normal file
113
alembic.ini
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
# 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
|
59
alembic/env.py
Normal file
59
alembic/env.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
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()
|
26
alembic/script.py.mako
Normal file
26
alembic/script.py.mako
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
"""${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"}
|
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
|
109
poetry.lock
generated
109
poetry.lock
generated
|
@ -1,5 +1,24 @@
|
||||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
# 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]]
|
[[package]]
|
||||||
name = "annotated-types"
|
name = "annotated-types"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
|
@ -270,6 +289,94 @@ files = [
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
referencing = ">=0.31.0"
|
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]]
|
[[package]]
|
||||||
name = "ocpp"
|
name = "ocpp"
|
||||||
version = "0.26.0"
|
version = "0.26.0"
|
||||||
|
@ -957,4 +1064,4 @@ files = [
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "599bf05928701049ff4348ad877ab3413c9864c2a5bf41ae047c7bbe0704270f"
|
content-hash = "2b9d4a1a7ceaa46c78814156552aea406a0683f55a574d198d8a65917b476e40"
|
||||||
|
|
|
@ -13,6 +13,7 @@ fastapi = "^0.110.0"
|
||||||
uvicorn = {extras = ["standard"], version = "^0.28.0"}
|
uvicorn = {extras = ["standard"], version = "^0.28.0"}
|
||||||
websockets = "^12.0"
|
websockets = "^12.0"
|
||||||
sqlalchemy = "^2.0.28"
|
sqlalchemy = "^2.0.28"
|
||||||
|
alembic = "^1.13.1"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
|
|
|
@ -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