Add firmware update logic
All checks were successful
ci/woodpecker/push/docker Pipeline was successful

This commit is contained in:
Oliver Traber 2025-03-21 16:40:49 +00:00 committed by BluemediaDev
parent 486977f828
commit 25c6556f42
Signed by: Bluemedia
GPG key ID: C0674B105057136C
8 changed files with 250 additions and 2 deletions

View file

@ -0,0 +1,50 @@
"""Add firmware_update table
Revision ID: 00edfb13e611
Revises: c7f72154c90b
Create Date: 2025-03-23 14:01:14.029527+00:00
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '00edfb13e611'
down_revision: Union[str, None] = 'c7f72154c90b'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('firmware_updates',
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('request_id', sa.Integer(), nullable=True),
sa.Column('status', sa.Enum('CREATED', 'SUBMITTED', 'DOWNLOADED', 'DOWNLOAD_FAILED', 'DOWNLOADING', 'DOWNLOAD_SCHEDULED', 'DOWNLOAD_PAUSED', 'IDLE', 'INSTALLATION_FAILED', 'INSTALLING', 'INSTALLED', 'INSTALL_REBOOTING', 'INSTALL_SCHEDULED', 'INSTALL_VERIFICATION_FAILED', 'INVALID_SIGNATURE', 'SIGNATURE_VERIFIED', name='firmwareupdatestatus'), nullable=True),
sa.Column('retries', sa.Integer(), nullable=True),
sa.Column('retry_interval', sa.Integer(), nullable=True),
sa.Column('location', sa.String(), nullable=True),
sa.Column('retrieve_date_time', sa.DateTime(), nullable=True),
sa.Column('install_date_time', sa.DateTime(), nullable=True),
sa.Column('chargepoint_id', sa.Uuid(), nullable=True),
sa.ForeignKeyConstraint(['chargepoint_id'], ['chargepoints.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_firmware_updates_chargepoint_id'), 'firmware_updates', ['chargepoint_id'], unique=False)
op.alter_column('users', 'is_active',
existing_type=sa.BOOLEAN(),
nullable=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('users', 'is_active',
existing_type=sa.BOOLEAN(),
nullable=True)
op.drop_index(op.f('ix_firmware_updates_chargepoint_id'), table_name='firmware_updates')
op.drop_table('firmware_updates')
# ### end Alembic commands ###

View file

@ -2,6 +2,7 @@ __all__ = [
"chargepoint_variable",
"chargepoint",
"connector",
"firmware_update",
"id_token",
"meter_value",
"session",

View file

@ -0,0 +1,20 @@
import uuid
from sqlalchemy import Column, DateTime, Enum, ForeignKey, Integer, String, Uuid
from app.database import Base
from app.schemas.firmware_update import FirmwareUpdateStatus
class FirmwareUpdate(Base):
__tablename__ = "firmware_updates"
id = Column(Uuid, primary_key=True, default=uuid.uuid4)
request_id = Column(Integer)
status = Column(Enum(FirmwareUpdateStatus))
retries = Column(Integer)
retry_interval = Column(Integer)
location = Column(String)
retrieve_date_time = Column(DateTime)
install_date_time = Column(DateTime, nullable=True)
chargepoint_id = Column(Uuid, ForeignKey("chargepoints.id"), index=True)

View file

@ -8,7 +8,7 @@ class Session(Base):
id = Column(Uuid, primary_key=True, default=uuid.uuid4)
name = Column(String)
refresh_token = Column(String, unique=True, index=True)
refresh_token = Column(String, nullable=False, unique=True, index=True)
last_used = Column(DateTime(timezone=True))
user_id = Column(Uuid, ForeignKey("users.id"), index=True)
user_id = Column(Uuid, ForeignKey("users.id"), nullable=False, index=True)

View file

@ -8,6 +8,7 @@ from ocpp.v201.enums import Action, RegistrationStatusEnumType, TransactionEvent
from ocpp.v201.call import GetBaseReport
from app.services import (
firmware_service,
variable_service,
id_token_service,
chargepoint_service,
@ -108,6 +109,11 @@ class ChargePoint(cp):
return call_result.TransactionEvent()
else:
return call_result.TransactionEvent(id_token_info=id_token_info)
@on(Action.firmware_status_notification)
async def on_firmware_status_notification(self, status, request_id, **kwargs):
await firmware_service.update_firmware_status(self.id, request_id, status)
return call_result.FirmwareStatusNotification()
@on(Action.meter_values)
async def on_meter_values(self, **kwargs):

View file

@ -28,10 +28,13 @@ from app.schemas.chargepoint_variable import (
MutabilityType,
SetVariableStatusType
)
from app.schemas.firmware_update import FirmwareUpdate, FirmwareUpdateCreate, FirmwareUpdateSubmissionResponse
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.models.firmware_update import FirmwareUpdate as DbFirmwareUpdate
from app.security.jwt_bearer import JWTBearer
from app.services import firmware_service
router = APIRouter(
prefix="/chargepoints",
@ -293,3 +296,67 @@ async def update_chargepoint_variable(
return ChargepointVariableResponse(status=status)
except TimeoutError:
raise HTTPException(status_code=503, detail="Chargepoint didn't respond in time.")
@router.get(path="/{chargepoint_id}/firmware-updates", response_model=list[FirmwareUpdate])
async def get_firmware_updates(
chargepoint_id: UUID,
db: Session = Depends(get_db),
token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
):
chargepoint = db.get(DbChargePoint, chargepoint_id)
if chargepoint is None:
raise HTTPException(status_code=404, detail="Chargepoint not found")
firmware_updates = db.query(DbFirmwareUpdate).filter(
DbFirmwareUpdate.chargepoint_id == chargepoint_id
).all()
return firmware_updates
@router.get(path="/{chargepoint_id}/firmware-updates/{firmware_update_id}", response_model=FirmwareUpdate)
async def get_firmware_update(
chargepoint_id: UUID,
firmware_update_id: UUID,
db: Session = Depends(get_db),
token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
):
chargepoint = db.get(DbChargePoint, chargepoint_id)
if chargepoint is None:
raise HTTPException(status_code=404, detail="Chargepoint not found")
firmware_update = db.query(DbFirmwareUpdate).filter(
DbFirmwareUpdate.chargepoint_id == chargepoint_id,
DbFirmwareUpdate.id == firmware_update_id
).first()
if firmware_update is None:
raise HTTPException(status_code=404, detail="FirmwareUpdate not found")
return firmware_update
@router.post(path="/{chargepoint_id}/firmware-updates", response_model=FirmwareUpdate)
async def create_firmware_update(
chargepoint_id: UUID,
firmware_update: FirmwareUpdateCreate,
db: Session = Depends(get_db),
token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
):
chargepoint = db.get(DbChargePoint, chargepoint_id)
if chargepoint is None:
raise HTTPException(status_code=404, detail="Chargepoint not found")
firmware_update = await firmware_service.create_firmware_update(chargepoint_id, firmware_update)
return firmware_update
@router.post(path="/{chargepoint_id}/firmware-updates/{firmware_update_id}/submit", response_model=ChargePointResetResponse)
async def submit_firmware_update(
chargepoint_id: UUID,
firmware_update_id: UUID,
token: AccessToken = Depends(JWTBearer(required_roles=["administrator"])),
):
if chargepoint_manager.is_connected(chargepoint_id) == False:
raise HTTPException(status_code=503, detail="Chargepoint not connected.")
try:
firmware_update, status = await firmware_service.submit_firmware_update(firmware_update_id)
return FirmwareUpdateSubmissionResponse(firmware_update, status)
except TimeoutError:
raise HTTPException(status_code=503, detail="Chargepoint didn't respond in time.")

View file

@ -0,0 +1,42 @@
from datetime import datetime
import enum
from typing import Optional
from uuid import UUID
from pydantic import BaseModel
class FirmwareUpdateStatus(enum.Enum):
CREATED = "xCreated"
SUBMITTED = "xSubmitted"
DOWNLOADED = "Downloaded"
DOWNLOAD_FAILED = "DownloadFailed"
DOWNLOADING = "Downloading"
DOWNLOAD_SCHEDULED = "DownloadScheduled"
DOWNLOAD_PAUSED = "DownloadPaused"
IDLE = "Idle"
INSTALLATION_FAILED = "InstallationFailed"
INSTALLING = "Installing"
INSTALLED = "Installed"
INSTALL_REBOOTING = "InstallRebooting"
INSTALL_SCHEDULED = "InstallScheduled"
INSTALL_VERIFICATION_FAILED = "InstallVerificationFailed"
INVALID_SIGNATURE = "InvalidSignature"
SIGNATURE_VERIFIED = "SignatureVerified"
class FirmwareUpdateBase(BaseModel):
retries: int
retry_interval: int
location: str
retrieve_date_time: datetime
install_date_time: Optional[datetime]
class FirmwareUpdate(FirmwareUpdateBase):
id: UUID
request_id: int
status: FirmwareUpdateStatus
class FirmwareUpdateCreate(FirmwareUpdateBase):
pass
class FirmwareUpdateSubmissionResponse(BaseModel):
firmware_update: FirmwareUpdate
status: str

View file

@ -0,0 +1,62 @@
from uuid import UUID
from ocpp.v201.call import UpdateFirmware
from ocpp.v201.call_result import UpdateFirmware as UpdateFirmwareResult
from ocpp.v201.datatypes import FirmwareType
from app.database import SessionLocal
from app.models.chargepoint import ChargePoint
from app.models.firmware_update import FirmwareUpdate
from app.ocpp_proto import chargepoint_manager
from app.schemas.firmware_update import FirmwareUpdateCreate, FirmwareUpdateStatus
async def create_firmware_update(chargepoint_id: UUID, firmware_update: FirmwareUpdateCreate) -> FirmwareUpdate:
with SessionLocal() as db:
db_chargepoint = db.get(ChargePoint, chargepoint_id)
latest_firmware_update = db.query(FirmwareUpdate).filter(FirmwareUpdate.chargepoint_id == db_chargepoint.id).order_by(FirmwareUpdate.request_id.desc()).first()
new_request_id = latest_firmware_update.request_id + 1 if latest_firmware_update else 1
db_firmware_update = FirmwareUpdate(
request_id=new_request_id,
status=FirmwareUpdateStatus.CREATED,
retries=firmware_update.retries,
retry_interval=firmware_update.retry_interval,
location=firmware_update.location,
retrieve_date_time=firmware_update.retrieve_date_time,
install_date_time=firmware_update.install_date_time,
chargepoint_id=db_chargepoint.id
)
db.add(db_firmware_update)
db.commit()
db.refresh(db_firmware_update)
return db_firmware_update
async def submit_firmware_update(firmware_update_id: UUID) -> tuple[FirmwareUpdate, str]:
with SessionLocal() as db:
db_firmware_update = db.get(FirmwareUpdate, firmware_update_id)
try:
result: UpdateFirmwareResult = await chargepoint_manager.call(
db_firmware_update.chargepoint_id,
payload=UpdateFirmware(
request_id=db_firmware_update.request_id,
retries=db_firmware_update.retries,
retry_interval=db_firmware_update.retry_interval,
firmware=FirmwareType(
location=db_firmware_update.location,
retrieve_date_time=db_firmware_update.retrieve_date_time,
install_date_time=db_firmware_update.install_date_time
)
))
if result.status == "Accepted" or result.status == "AcceptedCanceled":
db_firmware_update.status = FirmwareUpdateStatus.SUBMITTED
db.commit()
return db_firmware_update, result.status
except TimeoutError as e:
raise e
async def update_firmware_status(chargepoint_identity: str, request_id: int, status: FirmwareUpdateStatus):
with SessionLocal() as db:
db_chargepoint = db.query(ChargePoint).filter(ChargePoint.identity == chargepoint_identity).first()
db_firmware_update = db.query(FirmwareUpdate).filter(FirmwareUpdate.chargepoint_id == db_chargepoint.id).filter(FirmwareUpdate.request_id == request_id).first()
db_firmware_update.status = status
db.commit()