diff --git a/app/models/__init__.py b/app/models/__init__.py index 0f3f4ba..b6ddc91 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,4 +1,5 @@ __all__ = [ + "chargepoint_variable", "chargepoint", "connector", "id_token", diff --git a/app/models/chargepoint.py b/app/models/chargepoint.py index a6f6354..e24e180 100644 --- a/app/models/chargepoint.py +++ b/app/models/chargepoint.py @@ -23,3 +23,5 @@ class ChargePoint(Base): learn_until = Column(DateTime, nullable=True) connectors = relationship("Connector", cascade="delete, delete-orphan") + transactions = relationship("Transaction", cascade="delete, delete-orphan") + variables = relationship("ChargepointVariable", cascade="delete, delete-orphan") diff --git a/app/models/chargepoint_variable.py b/app/models/chargepoint_variable.py new file mode 100644 index 0000000..d516b9a --- /dev/null +++ b/app/models/chargepoint_variable.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass +import uuid +from sqlalchemy import ForeignKey, Integer, Numeric, Uuid, Boolean, Column, String, Enum + +from app.database import Base +from app.schemas.chargepoint_variable import AttributeType, MutabilityType, DataType + +@dataclass +class ChargepointVariable(Base): + __tablename__ = "chargepoint_variables" + + id = Column(Uuid, primary_key=True, default=uuid.uuid4) + name = Column(String) + type = Column(Enum(AttributeType), default=AttributeType.ACTUAL) + value = Column(String, nullable=True) + mutability = Column(Enum(MutabilityType), default=MutabilityType.READ_WRITE) + persistent = Column(Boolean, default=False) + constant = Column(Boolean, default=False) + unit = Column(String, nullable=True) + data_type = Column(Enum(DataType), nullable=True) + min_limit = Column(Numeric, nullable=True) + max_limit = Column(Numeric, nullable=True) + values_list = Column(String, nullable=True) + component_name = Column(String) + component_instance = Column(String, nullable=True) + evse = Column(Integer, nullable=True) + connector_id = Column(Integer, nullable=True) + + chargepoint_id = Column(Uuid, ForeignKey("chargepoints.id"), index=True) \ No newline at end of file diff --git a/app/ocpp_proto/chargepoint.py b/app/ocpp_proto/chargepoint.py index 18978a0..51e125d 100644 --- a/app/ocpp_proto/chargepoint.py +++ b/app/ocpp_proto/chargepoint.py @@ -2,11 +2,12 @@ from datetime import datetime, UTC import os from uuid import UUID -from ocpp.routing import on +from ocpp.routing import on, after from ocpp.v201 import ChargePoint as cp from ocpp.v201 import call_result from ocpp.v201.datatypes import IdTokenInfoType, IdTokenType from ocpp.v201.enums import Action, RegistrationStatusType, AuthorizationStatusType, IdTokenType as IdTokenEnumType, TransactionEventType +from ocpp.v201.call import GetBaseReportPayload from app.database import SessionLocal from app.models.chargepoint import ChargePoint as DbChargePoint @@ -17,6 +18,7 @@ from app.models.meter_value import MeterValue as DbMeterValue from app.schemas.connector import ConnectorStatus from app.schemas.transaction import TransactionStatus, TransactionEventTriggerReason from app.schemas.meter_value import Measurand, PhaseType +from app.ocpp_proto.variable_manager import create_or_update_variable class ChargePoint(cp): @@ -35,21 +37,43 @@ class ChargePoint(cp): with SessionLocal() as db: db_id_token = db.query(DbIdToken).filter(DbIdToken.token == id_token["id_token"]).first() if db_id_token == None: - return IdTokenInfoType( + id_token_info = IdTokenInfoType( status=AuthorizationStatusType.unknown ) - if db_id_token.is_active == False: - id_token_info=IdTokenInfoType( - status=AuthorizationStatusType.blocked - ) + db_chargepoint = db.query(DbChargePoint).filter(DbChargePoint.identity == self.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 = DbIdToken() + db_id_token.friendly_name = "New token learned by {}".format(self.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=AuthorizationStatusType.accepted, + group_id_token=IdTokenType( + type=IdTokenEnumType.central, + id_token=str(db_id_token.owner_id) + ) + ) + db_chargepoint.learn_user_id = None + db_chargepoint.learn_until = None + db.commit() else: - id_token_info=IdTokenInfoType( - status=AuthorizationStatusType.accepted, - group_id_token=IdTokenType( - type=IdTokenEnumType.central, - id_token=str(db_id_token.owner_id) + if db_id_token.is_active == False: + id_token_info=IdTokenInfoType( + status=AuthorizationStatusType.blocked + ) + else: + id_token_info=IdTokenInfoType( + status=AuthorizationStatusType.accepted, + group_id_token=IdTokenType( + type=IdTokenEnumType.central, + id_token=str(db_id_token.owner_id) + ) ) - ) return id_token_info @on(Action.BootNotification) @@ -67,6 +91,21 @@ class ChargePoint(cp): status=RegistrationStatusType.accepted ) + @after(Action.BootNotification) + async def after_boot_notification(self, **kwargs): + await self.call(payload=GetBaseReportPayload(request_id=0, report_base="FullInventory")) + + @on(Action.NotifyReport) + async def on_notify_report(self, report_data, **kwargs): + with SessionLocal() as db: + db_chargepoint = db.query(DbChargePoint).filter(DbChargePoint.identity == self.id).first() + for entry in report_data: + await create_or_update_variable( + chargepoint_id=db_chargepoint.id, + report_data=entry + ) + return call_result.NotifyReportPayload() + @on(Action.Heartbeat) async def on_heartbeat_request(self): await self.__update_last_seen() diff --git a/app/ocpp_proto/variable_manager.py b/app/ocpp_proto/variable_manager.py new file mode 100644 index 0000000..fb7c4b9 --- /dev/null +++ b/app/ocpp_proto/variable_manager.py @@ -0,0 +1,64 @@ +from decimal import Decimal +from uuid import UUID + +from app.database import SessionLocal +from app.models.chargepoint_variable import ChargepointVariable as DbChargepointVariable +from app.schemas.chargepoint_variable import AttributeType, DataType, MutabilityType + +async def create_or_update_variable(chargepoint_id: UUID, report_data): + with SessionLocal() as db: + for variable_attribute in report_data['variable_attribute']: + query = db.query(DbChargepointVariable).filter( + DbChargepointVariable.chargepoint_id == chargepoint_id, + DbChargepointVariable.component_name == report_data['component']['name'], + DbChargepointVariable.name == report_data['variable']['name'] + ) + if "instance" in report_data['component'].keys(): + query = query.filter(DbChargepointVariable.component_instance == report_data['component']['instance']) + if "evse" in report_data['component'].keys(): + query = query.filter(DbChargepointVariable.evse == report_data['component']['evse']['id']) + if "connectorId" in report_data['component']['evse'].keys(): + query = query.filter(DbChargepointVariable.connector_id == report_data['component']['evse']['connectorId']) + if "type" in variable_attribute.keys(): + query = query.filter(DbChargepointVariable.type == AttributeType(variable_attribute['type'])) + else: + query = query.filter(DbChargepointVariable.type == AttributeType.ACTUAL) + db_variable = query.first() + if db_variable == None: + db_variable = DbChargepointVariable() + db_variable.chargepoint_id = chargepoint_id + db_variable.component_name = report_data['component']['name'] + db_variable.name = report_data['variable']['name'] + + if "value" in variable_attribute.keys(): + db_variable.value = variable_attribute['value'] + if "instance" in report_data['component'].keys(): + db_variable.component_instance = report_data['component']['instance'] + if "evse" in report_data['component'].keys(): + db_variable.evse = report_data['component']['evse']['id'] + if "connector_id" in report_data['component']['evse'].keys(): + db_variable.connector_id = report_data['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_data.keys(): + db_variable.data_type = DataType(report_data['variable_characteristics']['data_type']) + if "min_limit" in report_data['variable_characteristics'].keys(): + db_variable.min_limit = Decimal(report_data['variable_characteristics']['min_limit']) + if "max_limit" in report_data['variable_characteristics'].keys(): + db_variable.max_limit = Decimal(report_data['variable_characteristics']['max_limit']) + if "unit" in report_data['variable_characteristics'].keys(): + db_variable.unit = report_data['variable_characteristics']['unit'] + if "values_list" in report_data['variable_characteristics'].keys(): + db_variable.values_list = report_data['variable_characteristics']['values_list'] + db.add(db_variable) + else: + if "value" in variable_attribute.keys(): + db_variable.value = variable_attribute['value'] + db.commit() + \ No newline at end of file diff --git a/app/routers/chargepoint_v1.py b/app/routers/chargepoint_v1.py index ed49fdc..1460a6c 100644 --- a/app/routers/chargepoint_v1.py +++ b/app/routers/chargepoint_v1.py @@ -6,7 +6,7 @@ from fastapi import APIRouter, HTTPException, Security from fastapi.params import Depends from sqlalchemy.orm import Session -from ocpp.v201.call import ResetPayload +from ocpp.v201.call import ResetPayload, SetVariablesPayload from app.database import get_db from app.ocpp_proto import chargepoint_manager @@ -20,8 +20,16 @@ from app.schemas.chargepoint import ( ChargePointResetResponse ) from app.schemas.id_token import IdTokenLearnRequest, IdTokenLearnResponse +from app.schemas.chargepoint_variable import ( + ChargepointVariable, + ChargepointVariableUpdate, + ChargepointVariableResponse, + MutabilityType, + SetVariableStatusType +) from app.models.chargepoint import ChargePoint as DbChargePoint from app.models.user import User as DbUser +from app.models.chargepoint_variable import ChargepointVariable as DbChargepointVariable from app.security import get_api_key router = APIRouter( @@ -213,3 +221,74 @@ async def get_id_token_learn_request( db.commit() return [] + +@router.get(path="/{chargepoint_id}/variables", response_model=list[ChargepointVariable]) +async def get_chargepoint_variables( + chargepoint_id: UUID, + api_key: str = Security(get_api_key), + db: Session = Depends(get_db) +): + chargepoint = db.get(DbChargePoint, chargepoint_id) + if chargepoint is None: + raise HTTPException(status_code=404, detail="Chargepoint not found") + + return db.query(DbChargepointVariable).filter(DbChargepointVariable.chargepoint_id == chargepoint_id).all() + +@router.put(path="/{chargepoint_id}/variables/{variable_id}", response_model=ChargepointVariableResponse) +async def update_chargepoint_variable( + chargepoint_id: UUID, + variable_id: UUID, + variable_update: ChargepointVariableUpdate, + api_key: str = Security(get_api_key), + db: Session = Depends(get_db) +): + chargepoint = db.get(DbChargePoint, chargepoint_id) + if chargepoint is None: + raise HTTPException(status_code=404, detail="Chargepoint not found") + + variable = db.query(DbChargepointVariable).filter( + DbChargepointVariable.chargepoint_id == chargepoint_id, + DbChargepointVariable.id == variable_id + ).first() + if variable is None: + raise HTTPException(status_code=404, detail="ChargepointVariable not found") + if variable.mutability == MutabilityType.READ_ONLY: + raise HTTPException(status_code=422, detail="ChargepointVariable is read-only") + + variable.value = variable_update.value + + if chargepoint_manager.is_connected(chargepoint_id) == False: + raise HTTPException(status_code=503, detail="Chargepoint not connected.") + try: + evse = None + if variable.evse != None: + evse = { + 'id': variable.evse + } + if variable.connector_id != None: + evse['connectorId'] = variable.connector_id + result = await chargepoint_manager.call( + chargepoint_id, + payload=SetVariablesPayload(set_variable_data=[ + { + 'attributeType': variable.type.value, + 'attributeValue': variable_update.value, + 'component': { + 'name': variable.component_name, + 'instance': variable.component_instance, + 'evse': evse + }, + 'variable': { + 'name': variable.name + } + } + ]) + ) + status = result.set_variable_result[0]['attribute_status'] + if SetVariableStatusType(status) in [SetVariableStatusType.ACCEPTED, SetVariableStatusType.REBOOT_REQUIRED]: + db.commit() + else: + raise HTTPException(status_code=500, detail=status) + return ChargepointVariableResponse(status=status) + except TimeoutError: + raise HTTPException(status_code=503, detail="Chargepoint didn't respond in time.") diff --git a/app/schemas/chargepoint_variable.py b/app/schemas/chargepoint_variable.py new file mode 100644 index 0000000..19200eb --- /dev/null +++ b/app/schemas/chargepoint_variable.py @@ -0,0 +1,60 @@ +from decimal import Decimal +from typing import Optional +from uuid import UUID +from pydantic import BaseModel +import enum + +class AttributeType(enum.Enum): + ACTUAL = "Actual" + TARGET = "Target" + MIN_SET = "MinSet" + MAX_SET = "MaxSet" + +class MutabilityType(enum.Enum): + READ_ONLY = "ReadOnly" + WRITE_ONLY = "WriteOnly" + READ_WRITE = "ReadWrite" + +class DataType(enum.Enum): + STRING = "string" + DECIMAL = "decimal" + INTEGER = "integer" + DATETIME = "dateTime" + BOOLEAN = "boolean" + OPTION_LIST = "OptionList" + SEQUENCE_LIST = "SequenceList" + MEMBER_LIST = "MemberList" + +class SetVariableStatusType(enum.Enum): + ACCEPTED = "Accepted" + REJECTED = "Rejected" + UNKNOWN_COMPONENT = "UnknownComponent" + NOT_SUPPORTED_ATTRIBUTE_TYPE = "NotSupportedAttributeType" + REBOOT_REQUIRED = "RebootRequired" + +class ChargepointVariable(BaseModel): + id: UUID + name: str + type: AttributeType + value: Optional[str] = None + mutability: MutabilityType + persistent: bool + constant: bool + unit: Optional[str] = None + data_type: Optional[DataType] = None + min_limit: Optional[Decimal] = None + max_limit: Optional[Decimal] = None + values_list: Optional[str] = None + component_name: str + component_instance: Optional[str] = None + evse: Optional[int] = None + connector_id: Optional[int] = None + + class Config: + from_attributes = True + +class ChargepointVariableUpdate(BaseModel): + value: str + +class ChargepointVariableResponse(BaseModel): + status: SetVariableStatusType