Prepare monorepo

This commit is contained in:
Oliver Traber 2025-03-13 22:11:20 +01:00
parent a1ddb43ed0
commit 938582155d
Signed by: Bluemedia
GPG key ID: C0674B105057136C
61 changed files with 5 additions and 5 deletions

View file

View file

@ -0,0 +1,46 @@
from datetime import datetime, UTC
from app.database import SessionLocal
from app.models.chargepoint import ChargePoint
from app.models.connector import Connector
from app.schemas.connector import ConnectorStatus
async def update_last_seen(chargepoint_identity: str):
with SessionLocal() as db:
db_chargepoint = db.query(ChargePoint).filter(ChargePoint.identity == chargepoint_identity).first()
db_chargepoint.last_seen = datetime.now(UTC)
db.commit()
async def update_attributes(chargepoint_identity: str, charging_station):
with SessionLocal() as db:
db_chargepoint = db.query(ChargePoint).filter(ChargePoint.identity == chargepoint_identity).first()
for key in charging_station.keys():
if key in db_chargepoint.__dict__:
setattr(db_chargepoint, key, charging_station[key])
db.commit()
async def create_or_update_connector(
chargepoint_identity: str,
evse_id: int,
connector_id: int,
connector_status: str
):
with SessionLocal() as db:
db_chargepoint = db.query(ChargePoint).filter(ChargePoint.identity == chargepoint_identity).first()
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()

View file

@ -0,0 +1,51 @@
from datetime import datetime, UTC
from ocpp.v201.datatypes import IdTokenInfoType
from ocpp.v201.enums import AuthorizationStatusEnumType
from app.database import SessionLocal
from app.models.id_token import IdToken
from app.models.chargepoint import ChargePoint
async def get_id_token_info(chargepoint_id: str, id_token: str):
owner_id = None
if id_token["type"] not in ["ISO14443", "ISO15693"]:
return IdTokenInfoType(
status=AuthorizationStatusEnumType.invalid
), owner_id
with SessionLocal() as db:
db_id_token = db.query(IdToken).filter(IdToken.token == id_token["id_token"]).first()
if db_id_token == None:
id_token_info = IdTokenInfoType(
status=AuthorizationStatusEnumType.unknown
)
db_chargepoint = db.query(ChargePoint).filter(ChargePoint.identity == chargepoint_id).first()
# Learn token if requested
if db_chargepoint.learn_user_id != None:
if db_chargepoint.learn_until.timestamp() > datetime.now(UTC).timestamp():
db_id_token = IdToken()
db_id_token.friendly_name = "New token learned by {}".format(chargepoint_id)
db_id_token.is_active = True
db_id_token.owner_id = db_chargepoint.learn_user_id
db_id_token.token = id_token["id_token"]
db.add(db_id_token)
id_token_info=IdTokenInfoType(
status=AuthorizationStatusEnumType.accepted
)
owner_id = db_id_token.owner_id
db_chargepoint.learn_user_id = None
db_chargepoint.learn_until = None
db.commit()
else:
owner_id = db_id_token.owner_id
if db_id_token.is_active == False:
id_token_info=IdTokenInfoType(
status=AuthorizationStatusEnumType.blocked
)
else:
id_token_info=IdTokenInfoType(
status=AuthorizationStatusEnumType.accepted
)
return id_token_info, owner_id

View file

@ -0,0 +1,30 @@
from datetime import datetime
from uuid import UUID
from app.database import SessionLocal
from app.models.meter_value import MeterValue
from app.schemas.meter_value import Measurand, PhaseType
async def create_meter_value(transaction_id: UUID, meter_value_data):
with SessionLocal() as db:
timestamp = datetime.fromisoformat(meter_value_data['timestamp'])
for sampled_value in meter_value_data['sampled_value']:
db_meter_value = MeterValue()
db_meter_value.transaction_id = transaction_id
db_meter_value.timestamp = timestamp
if "measurand" in sampled_value.keys():
db_meter_value.measurand = Measurand(sampled_value['measurand'])
else:
db_meter_value.measurand = Measurand.ENERGY_ACTIVE_IMPORT_REGISTER
if "phase" in sampled_value.keys():
db_meter_value.phase_type = PhaseType(sampled_value['phase'])
if "unit_of_measure" in sampled_value.keys():
if "unit" in sampled_value['unit_of_measure']:
db_meter_value.unit = sampled_value['unit_of_measure']['unit']
else:
db_meter_value.unit = "Wh"
db_meter_value.value = sampled_value['value']
db.add(db_meter_value)
db.commit()

View file

@ -0,0 +1,83 @@
from datetime import datetime, UTC
import secrets
from uuid import UUID
from sqlalchemy import select, delete
from sqlalchemy.orm import Session as SqlaSession
from app.models.session import Session
from app.models.user import User
from app.util.errors import NotFoundError
async def get_sessions(
db: SqlaSession, skip: int = 0, limit: int = 20
) -> tuple[Session]:
stmt = select(Session).offset(skip).limit(limit)
result = db.execute(stmt)
return result.scalars().all()
async def get_sessions_by_user(db: SqlaSession, user_id: UUID) -> tuple[Session]:
stmt = select(Session).where(Session.user_id == user_id)
result = db.execute(stmt)
return result.scalars().all()
async def create_session(db: SqlaSession, user: User, useragent: str) -> Session:
session = Session(
name=useragent,
refresh_token=secrets.token_urlsafe(64),
last_used=datetime.now(UTC),
user_id=user.id,
)
db.add(session)
db.commit()
db.refresh(session)
return session
async def remove_session(db: SqlaSession, id: UUID):
session = db.get(Session, id)
if not session:
raise NotFoundError
db.delete(session)
db.commit()
async def remove_session_for_user(
db: SqlaSession, id: UUID, user_id: UUID
):
stmt = select(Session).where(Session.id == id and Session.user_id == user_id)
result = db.execute(stmt)
session = result.scalars().first()
if not session:
raise NotFoundError
db.delete(session)
db.commit()
async def remove_all_sessions_for_user(db: SqlaSession, user_id: UUID):
stmt = delete(Session).where(Session.user_id == user_id)
db.execute(stmt)
db.commit()
async def remove_all_sessions(db: SqlaSession):
stmt = delete(Session)
db.execute(stmt)
db.commit()
async def validate_and_rotate_refresh_token(
db: SqlaSession, refresh_token: str
) -> Session:
stmt = select(Session).where(Session.refresh_token == refresh_token)
result = db.execute(stmt)
session = result.scalars().first()
if not session:
raise NotFoundError
session.refresh_token = secrets.token_urlsafe(64)
session.last_used = datetime.now(UTC)
db.commit()
return session

View file

@ -0,0 +1,69 @@
import json
import os
import secrets
from typing import Optional
from uuid import UUID
from jwcrypto import jwt, jwk
from datetime import datetime, timedelta, UTC
from app.models.user import User
from app.schemas.auth_token import AccessToken
from app.schemas.user import Role
from app.util.errors import InsufficientPermissionsError, InvalidTokenAudienceError
__signing_key = jwk.JWK.from_password(os.getenv("CS_TOKEN_SECRET", secrets.token_urlsafe(64)))
async def __create_token(claims: dict) -> str:
default_claims = {
"iss": os.getenv("CS_TOKEN_ISSUER", "https://localhost:8000"),
"iat": datetime.now(UTC).timestamp(),
}
header = {"alg": "HS256", "typ": "JWT", "kid": "default"}
token = jwt.JWT(header=header, claims=(claims | default_claims))
token.make_signed_token(__signing_key)
return token.serialize()
async def __verify_token(token: str, audience: str) -> dict | None:
try:
token = jwt.JWT(jwt=token, key=__signing_key)
claims = json.loads(token.claims)
if claims.get("aud") == audience:
return claims
else:
raise InvalidTokenAudienceError
except Exception:
return None
async def create_access_token(
user: User, session_id: UUID
) -> tuple[str, datetime]:
token_lifetime = float(os.getenv("CS_ACCESS_TOKEN_LIFETIME_SECONDS", "300"))
exp_time = datetime.now(UTC) + timedelta(seconds=token_lifetime)
claims = {
"aud": "access",
"sub": str(user.id),
"exp": exp_time.timestamp(),
"session": str(session_id),
"role": str(user.role),
}
return await __create_token(claims=claims), exp_time
async def verify_access_token(
token: str, required_roles: Optional[list[str]] = None
) -> AccessToken | None:
try:
claims = await __verify_token(token=token, audience="access")
if not claims:
return None
if not required_roles or claims.get("role") in required_roles:
return AccessToken(
subject=claims.get("sub"),
role=Role(claims.get("role")),
session=claims.get("session"),
)
else:
raise InsufficientPermissionsError
except InvalidTokenAudienceError:
pass

View file

@ -0,0 +1,86 @@
from datetime import datetime
from typing import Optional
from uuid import UUID
from app.database import SessionLocal
from app.models.chargepoint import ChargePoint
from app.models.transaction import Transaction
from app.schemas.meter_value import Measurand
from app.schemas.transaction import TransactionEventTriggerReason, TransactionStatus
from app.services import meter_value_service
async def create_transaction(
chargepoint_identity: str,
user_id: UUID,
timestamp: datetime,
transaction_info,
transaction_data
):
with SessionLocal() as db:
chargepoint = db.query(ChargePoint).filter(ChargePoint.identity == chargepoint_identity).first()
meter_start=0
if "meter_value" in transaction_data.keys():
for meter_value_entry in transaction_data['meter_value']:
for sampled_value in meter_value_entry['sampled_value']:
if "measurand" in sampled_value.keys():
if sampled_value['measurand'] == str(Measurand.ENERGY_ACTIVE_IMPORT_REGISTER):
meter_start = sampled_value['value']
else:
meter_start = sampled_value['value']
transaction = Transaction(
id=transaction_info["transaction_id"],
status=TransactionStatus.ONGOING,
started_at=timestamp,
meter_start=meter_start,
price=chargepoint.price,
chargepoint_id=chargepoint.id,
user_id=user_id
)
db.add(transaction)
db.commit()
async def update_transaction(
transaction_id: str,
transaction_data
):
with SessionLocal() as db:
transaction = db.get(Transaction, transaction_id)
if transaction != None:
if transaction.status == TransactionStatus.ONGOING:
if "meter_value" in transaction_data.keys():
for meter_value_entry in transaction_data['meter_value']:
await meter_value_service.create_meter_value(
transaction_id=transaction.id,
meter_value_data=meter_value_entry
)
async def end_transaction(
transaction_id: str,
timestamp: datetime,
trigger_reason: str,
transaction_data,
user_id: Optional[UUID]
):
with SessionLocal() as db:
transaction = db.get(Transaction, transaction_id)
if transaction != None:
meter_end=0
if "meter_value" in transaction_data.keys():
for meter_value_entry in transaction_data['meter_value']:
for sampled_value in meter_value_entry['sampled_value']:
if "measurand" in sampled_value.keys():
if sampled_value['measurand'] == str(Measurand.ENERGY_ACTIVE_IMPORT_REGISTER):
meter_end = sampled_value['value']
else:
meter_end = sampled_value['value']
transaction.status = TransactionStatus.ENDED
transaction.ended_at = timestamp
transaction.end_reason = TransactionEventTriggerReason(trigger_reason)
transaction.meter_end = meter_end
if user_id != None:
transaction.user_id = user_id
db.commit()

View file

@ -0,0 +1,111 @@
from uuid import UUID
import uuid
from sqlalchemy import select
from sqlalchemy.orm import Session
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
from app.models.user import User
from app.schemas.user import (
UserCreate,
UserUpdate,
AdministrativeUserUpdate,
PasswordUpdate,
LoginRequest,
)
from app.util.errors import InvalidStateError, NotFoundError
hasher = PasswordHasher(memory_cost=102400)
async def get_user(db: Session, id: UUID):
return db.get(User, id)
async def get_user_by_email(db: Session, email: str):
stmt = select(User).where(User.email == email)
result = db.execute(stmt)
return result.scalars().first()
async def get_users(
db: Session, skip: int = 0, limit: int = 20, email: str = None
):
stmt = select(User)
if email is not None:
stmt = stmt.where(User.email.like(email))
stmt = stmt.offset(skip).limit(limit)
result = db.execute(stmt)
return result.scalars().all()
async def create_user(db: Session, user: UserCreate) -> User:
if await get_user_by_email(db=db, email=user.email):
raise InvalidStateError
hashed_password = hasher.hash(user.password)
db_user = User(
friendly_name=user.friendly_name, email=user.email, password=hashed_password
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
async def update_user(
db: Session,
id: UUID,
update: UserUpdate | AdministrativeUserUpdate,
) -> User:
db_user = await get_user(db, id)
if db_user is None:
raise NotFoundError
changed_attributes = dict()
for key, value in update.model_dump(exclude_unset=True).items():
changed_attributes[key] = {"old": getattr(db_user, key), "new": value}
setattr(db_user, key, value)
db.commit()
return db_user
async def change_user_password(db: Session, id: UUID, update: PasswordUpdate):
db_user = await get_user(db, id)
if db_user is None:
raise NotFoundError
try:
hasher.verify(hash=db_user.password, password=update.old_password)
db_user.password = hasher.hash(update.new_password)
db.commit()
except VerifyMismatchError:
raise InvalidStateError
async def remove_user(db: Session, id: UUID):
db_user = await get_user(db, id)
if db_user is None:
raise NotFoundError
db.delete(db_user)
db.commit()
async def validate_login(db: Session, login: LoginRequest) -> User | None:
stmt = select(User).where(User.email == login.email)
result = db.execute(stmt)
db_user = result.scalars().first()
if db_user is None:
db.commit()
return None
try:
hasher.verify(hash=db_user.password, password=login.password)
if hasher.check_needs_rehash(db_user.password):
db_user.password = hasher.hash(login.password)
if db_user.is_active:
db.commit()
return db_user
else:
db.commit()
return None
except VerifyMismatchError:
db.commit()
return None

View file

@ -0,0 +1,66 @@
from decimal import Decimal
from app.database import SessionLocal
from app.models.chargepoint import ChargePoint
from app.models.chargepoint_variable import ChargepointVariable
from app.schemas.chargepoint_variable import AttributeType, DataType, MutabilityType
async def create_or_update_variable(chargepoint_identity: str, report_entry):
with SessionLocal() as db:
db_chargepoint = db.query(ChargePoint).filter(ChargePoint.identity == chargepoint_identity).first()
for variable_attribute in report_entry['variable_attribute']:
query = db.query(ChargepointVariable).filter(
ChargepointVariable.chargepoint_id == db_chargepoint.id,
ChargepointVariable.component_name == report_entry['component']['name'],
ChargepointVariable.name == report_entry['variable']['name']
)
if "instance" in report_entry['component'].keys():
query = query.filter(ChargepointVariable.component_instance == report_entry['component']['instance'])
if "evse" in report_entry['component'].keys():
query = query.filter(ChargepointVariable.evse == report_entry['component']['evse']['id'])
if "connectorId" in report_entry['component']['evse'].keys():
query = query.filter(ChargepointVariable.connector_id == report_entry['component']['evse']['connectorId'])
if "type" in variable_attribute.keys():
query = query.filter(ChargepointVariable.type == AttributeType(variable_attribute['type']))
else:
query = query.filter(ChargepointVariable.type == AttributeType.ACTUAL)
db_variable = query.first()
if db_variable == None:
db_variable = ChargepointVariable()
db_variable.chargepoint_id = db_chargepoint.id
db_variable.component_name = report_entry['component']['name']
db_variable.name = report_entry['variable']['name']
if "value" in variable_attribute.keys():
db_variable.value = variable_attribute['value']
if "instance" in report_entry['component'].keys():
db_variable.component_instance = report_entry['component']['instance']
if "evse" in report_entry['component'].keys():
db_variable.evse = report_entry['component']['evse']['id']
if "connector_id" in report_entry['component']['evse'].keys():
db_variable.connector_id = report_entry['component']['evse']['connector_id']
if "constant" in variable_attribute.keys():
db_variable.constant = variable_attribute['constant']
if "persistent" in variable_attribute.keys():
db_variable.constant = variable_attribute['persistent']
if "mutability" in variable_attribute.keys():
db_variable.mutability = MutabilityType(variable_attribute['mutability'])
if "type" in variable_attribute.keys():
db_variable.type = AttributeType(variable_attribute['type'])
if "variable_characteristics" in report_entry.keys():
db_variable.data_type = DataType(report_entry['variable_characteristics']['data_type'])
if "min_limit" in report_entry['variable_characteristics'].keys():
db_variable.min_limit = Decimal(report_entry['variable_characteristics']['min_limit'])
if "max_limit" in report_entry['variable_characteristics'].keys():
db_variable.max_limit = Decimal(report_entry['variable_characteristics']['max_limit'])
if "unit" in report_entry['variable_characteristics'].keys():
db_variable.unit = report_entry['variable_characteristics']['unit']
if "values_list" in report_entry['variable_characteristics'].keys():
db_variable.values_list = report_entry['variable_characteristics']['values_list']
db.add(db_variable)
else:
if "value" in variable_attribute.keys():
db_variable.value = variable_attribute['value']
db.commit()