From b892fed2c4832896332639b3e7a31bc0bc904e3c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:18:42 +0200 Subject: [PATCH] feat(ocpp2): implement GetBaseReport and NotifyReport commands (#1556) MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit * Initial plan * Add GetBaseReport OCPP 2.0 command implementation Co-authored-by: jerome-benoit <6537332+jerome-benoit@users.noreply.github.com> * fix: correct enum ordering and add NotifyReport handler to mock server Co-authored-by: jerome-benoit <6537332+jerome-benoit@users.noreply.github.com> * feat: add NotifyReport command and trigger from GetBaseReport Co-authored-by: jerome-benoit <6537332+jerome-benoit@users.noreply.github.com> * feat: populate NotifyReport with device model data based on reportBase Co-authored-by: jerome-benoit <6537332+jerome-benoit@users.noreply.github.com> * refactor: improve code quality with typing, validation and constants Co-authored-by: jerome-benoit <6537332+jerome-benoit@users.noreply.github.com> * refactor: export types, separate concerns, move constants Co-authored-by: jerome-benoit <6537332+jerome-benoit@users.noreply.github.com> * refactor: use event emission pattern and static class properties Co-authored-by: jerome-benoit <6537332+jerome-benoit@users.noreply.github.com> * refactor: use direct method call pattern for NotifyReport Co-authored-by: jerome-benoit <6537332+jerome-benoit@users.noreply.github.com> * refactor: use event emission pattern for GetBaseReport/NotifyReport Co-authored-by: jerome-benoit <6537332+jerome-benoit@users.noreply.github.com> * Update src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jerome-benoit <6537332+jerome-benoit@users.noreply.github.com> Co-authored-by: Jérôme Benoit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../ocpp/2.0/OCPP20Constants.ts | 7 + .../ocpp/2.0/OCPP20IncomingRequestService.ts | 261 ++++++++++++++++++ .../ocpp/2.0/OCPP20RequestService.ts | 16 ++ .../ocpp/2.0/OCPP20ResponseService.ts | 23 ++ src/types/index.ts | 17 +- src/types/ocpp/2.0/Common.ts | 13 + src/types/ocpp/2.0/Requests.ts | 49 ++++ src/types/ocpp/2.0/Responses.ts | 8 + tests/ocpp-server/server.py | 7 + 9 files changed, 400 insertions(+), 1 deletion(-) diff --git a/src/charging-station/ocpp/2.0/OCPP20Constants.ts b/src/charging-station/ocpp/2.0/OCPP20Constants.ts index c94fbe9f..93c37f40 100644 --- a/src/charging-station/ocpp/2.0/OCPP20Constants.ts +++ b/src/charging-station/ocpp/2.0/OCPP20Constants.ts @@ -5,6 +5,13 @@ import { import { OCPPConstants } from '../OCPPConstants.js' export class OCPP20Constants extends OCPPConstants { + static readonly ComponentName = { + CHARGING_STATION: 'ChargingStation', + CONNECTOR: 'Connector', + EVSE: 'EVSE', + OCPP_COMM_CTRLR: 'OCPPCommCtrlr', + } as const + static readonly ChargingStationStatusTransitions: readonly ConnectorStatusTransition[] = Object.freeze([ { to: OCPP20ConnectorStatusEnumType.Available }, diff --git a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts index 23f4c024..05ce66aa 100644 --- a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts @@ -6,15 +6,29 @@ import type { ChargingStation } from '../../../charging-station/index.js' import { OCPPError } from '../../../exception/index.js' import { + type ComponentType, ErrorType, + type EVSEType, + GenericDeviceModelStatusEnumType, type IncomingRequestHandler, type JsonType, type OCPP20ClearCacheRequest, + type OCPP20GetBaseReportRequest, + type OCPP20GetBaseReportResponse, OCPP20IncomingRequestCommand, + type OCPP20NotifyReportRequest, + type OCPP20NotifyReportResponse, + OCPP20RequestCommand, OCPPVersion, + ReportBaseEnumType, + type ReportDataType, + type VariableAttributeType, + type VariableCharacteristicsType, + type VariableType, } from '../../../types/index.js' import { isAsyncFunction, logger } from '../../../utils/index.js' import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService.js' +import { OCPP20Constants } from './OCPP20Constants.js' import { OCPP20ServiceUtils } from './OCPP20ServiceUtils.js' const moduleName = 'OCPP20IncomingRequestService' @@ -34,6 +48,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { super(OCPPVersion.VERSION_201) this.incomingRequestHandlers = new Map([ [OCPP20IncomingRequestCommand.CLEAR_CACHE, this.handleRequestClearCache.bind(this)], + [OCPP20IncomingRequestCommand.GET_BASE_REPORT, this.handleRequestGetBaseReport.bind(this)], ]) this.payloadValidateFunctions = new Map< OCPP20IncomingRequestCommand, @@ -49,7 +64,53 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { ) ), ], + [ + OCPP20IncomingRequestCommand.GET_BASE_REPORT, + this.ajv.compile( + OCPP20ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/2.0/GetBaseReportRequest.json', + moduleName, + 'constructor' + ) + ), + ], ]) + // Handle incoming request events + this.on( + OCPP20IncomingRequestCommand.GET_BASE_REPORT, + ( + chargingStation: ChargingStation, + request: OCPP20GetBaseReportRequest, + response: OCPP20GetBaseReportResponse + ) => { + if (response.status === GenericDeviceModelStatusEnumType.Accepted) { + const { requestId, reportBase } = request + const reportData = this.buildReportData(chargingStation, reportBase) + chargingStation.ocppRequestService + .requestHandler( + chargingStation, + OCPP20RequestCommand.NOTIFY_REPORT, + { + reportData, + requestId, + seqNo: 0, + tbc: false, + } + ) + .then(() => { + logger.info( + `${chargingStation.logPrefix()} ${moduleName}.constructor: NotifyReport sent for requestId ${requestId} with ${reportData.length} report items` + ) + }) + .catch((error: unknown) => { + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.constructor: NotifyReport error:`, + error + ) + }) + } + } + ) this.validatePayload = this.validatePayload.bind(this) } @@ -142,6 +203,206 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { this.emit(commandName, chargingStation, commandPayload, response) } + private handleRequestGetBaseReport ( + chargingStation: ChargingStation, + commandPayload: OCPP20GetBaseReportRequest + ): OCPP20GetBaseReportResponse { + logger.info( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetBaseReport: GetBaseReport request received with requestId ${commandPayload.requestId} and reportBase ${commandPayload.reportBase}` + ) + // Build report data to check if any data is available + const reportData = this.buildReportData(chargingStation, commandPayload.reportBase) + if (reportData.length === 0) { + logger.info( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetBaseReport: No data available for reportBase ${commandPayload.reportBase}` + ) + return { + status: GenericDeviceModelStatusEnumType.EmptyResultSet, + } + } + return { + status: GenericDeviceModelStatusEnumType.Accepted, + } + } + + private buildReportData ( + chargingStation: ChargingStation, + reportBase: string + ): ReportDataType[] { + // Validate reportBase parameter + if (!Object.values(ReportBaseEnumType).includes(reportBase as ReportBaseEnumType)) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.buildReportData: Invalid reportBase '${reportBase}'` + ) + return [] + } + + const reportData: ReportDataType[] = [] + + switch (reportBase) { + case ReportBaseEnumType.ConfigurationInventory: + // Include OCPP configuration keys + if (chargingStation.ocppConfiguration?.configurationKey) { + for (const configKey of chargingStation.ocppConfiguration.configurationKey) { + reportData.push({ + component: { + name: OCPP20Constants.ComponentName.OCPP_COMM_CTRLR, + }, + variable: { + name: configKey.key, + }, + variableAttribute: [ + { + type: 'Actual', + value: configKey.value, + }, + ], + variableCharacteristics: { + dataType: 'string', + supportsMonitoring: false, + }, + }) + } + } + break + + case ReportBaseEnumType.FullInventory: + // Include all device model variables + // 1. Station information + if (chargingStation.stationInfo) { + const stationInfo = chargingStation.stationInfo + if (stationInfo.chargePointModel) { + reportData.push({ + component: { name: OCPP20Constants.ComponentName.CHARGING_STATION }, + variable: { name: 'Model' }, + variableAttribute: [{ type: 'Actual', value: stationInfo.chargePointModel }], + variableCharacteristics: { dataType: 'string', supportsMonitoring: false }, + }) + } + if (stationInfo.chargePointVendor) { + reportData.push({ + component: { name: OCPP20Constants.ComponentName.CHARGING_STATION }, + variable: { name: 'VendorName' }, + variableAttribute: [{ type: 'Actual', value: stationInfo.chargePointVendor }], + variableCharacteristics: { dataType: 'string', supportsMonitoring: false }, + }) + } + if (stationInfo.chargePointSerialNumber) { + reportData.push({ + component: { name: OCPP20Constants.ComponentName.CHARGING_STATION }, + variable: { name: 'SerialNumber' }, + variableAttribute: [{ type: 'Actual', value: stationInfo.chargePointSerialNumber }], + variableCharacteristics: { dataType: 'string', supportsMonitoring: false }, + }) + } + if (stationInfo.firmwareVersion) { + reportData.push({ + component: { name: OCPP20Constants.ComponentName.CHARGING_STATION }, + variable: { name: 'FirmwareVersion' }, + variableAttribute: [{ type: 'Actual', value: stationInfo.firmwareVersion }], + variableCharacteristics: { dataType: 'string', supportsMonitoring: false }, + }) + } + } + + // 2. OCPP configuration + if (chargingStation.ocppConfiguration?.configurationKey) { + for (const configKey of chargingStation.ocppConfiguration.configurationKey) { + reportData.push({ + component: { name: OCPP20Constants.ComponentName.OCPP_COMM_CTRLR }, + variable: { name: configKey.key }, + variableAttribute: [{ type: 'Actual', value: configKey.value }], + variableCharacteristics: { dataType: 'string', supportsMonitoring: false }, + }) + } + } + + // 3. EVSE and connector information + if (chargingStation.evses.size > 0) { + for (const [evseId, evse] of chargingStation.evses) { + reportData.push({ + component: { + evse: { id: evseId }, + name: 'EVSE', + }, + variable: { name: 'AvailabilityState' }, + variableAttribute: [{ type: 'Actual', value: evse.availability }], + variableCharacteristics: { dataType: 'string', supportsMonitoring: true }, + }) + if (evse.connectors) { + for (const [connectorId, connector] of evse.connectors) { + reportData.push({ + component: { + evse: { connectorId, id: evseId }, + name: 'Connector', + }, + variable: { name: 'ConnectorType' }, + variableAttribute: [ + { type: 'Actual', value: String(connector.connectorType) }, + ], + variableCharacteristics: { dataType: 'string', supportsMonitoring: false }, + }) + } + } + } + } else if (chargingStation.connectors.size > 0) { + // Fallback to connectors if no EVSE structure + for (const [connectorId, connector] of chargingStation.connectors) { + if (connectorId > 0) { + reportData.push({ + component: { + evse: { connectorId, id: 1 }, + name: 'Connector', + }, + variable: { name: 'ConnectorType' }, + variableAttribute: [{ type: 'Actual', value: String(connector.connectorType) }], + variableCharacteristics: { dataType: 'string', supportsMonitoring: false }, + }) + } + } + } + break + + case ReportBaseEnumType.SummaryInventory: + // Include essential variables only + if (chargingStation.stationInfo) { + const stationInfo = chargingStation.stationInfo + if (stationInfo.chargePointModel) { + reportData.push({ + component: { name: OCPP20Constants.ComponentName.CHARGING_STATION }, + variable: { name: 'Model' }, + variableAttribute: [{ type: 'Actual', value: stationInfo.chargePointModel }], + variableCharacteristics: { dataType: 'string', supportsMonitoring: false }, + }) + } + if (stationInfo.chargePointVendor) { + reportData.push({ + component: { name: OCPP20Constants.ComponentName.CHARGING_STATION }, + variable: { name: 'VendorName' }, + variableAttribute: [{ type: 'Actual', value: stationInfo.chargePointVendor }], + variableCharacteristics: { dataType: 'string', supportsMonitoring: false }, + }) + } + if (stationInfo.firmwareVersion) { + reportData.push({ + component: { name: OCPP20Constants.ComponentName.CHARGING_STATION }, + variable: { name: 'FirmwareVersion' }, + variableAttribute: [{ type: 'Actual', value: stationInfo.firmwareVersion }], + variableCharacteristics: { dataType: 'string', supportsMonitoring: false }, + }) + } + } + break + + default: + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.buildReportData: Unknown reportBase '${reportBase}'` + ) + } + + return reportData + } + private validatePayload ( chargingStation: ChargingStation, commandName: OCPP20IncomingRequestCommand, diff --git a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts index 1e9253c9..57eeaca6 100644 --- a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts @@ -12,6 +12,7 @@ import { type JsonType, type OCPP20BootNotificationRequest, type OCPP20HeartbeatRequest, + type OCPP20NotifyReportRequest, OCPP20RequestCommand, type OCPP20StatusNotificationRequest, OCPPVersion, @@ -53,6 +54,16 @@ export class OCPP20RequestService extends OCPPRequestService { ) ), ], + [ + OCPP20RequestCommand.NOTIFY_REPORT, + this.ajv.compile( + OCPP20ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/2.0/NotifyReportRequest.json', + moduleName, + 'constructor' + ) + ), + ], [ OCPP20RequestCommand.STATUS_NOTIFICATION, this.ajv.compile( @@ -106,6 +117,11 @@ export class OCPP20RequestService extends OCPPRequestService { return commandParams as unknown as Request case OCPP20RequestCommand.HEARTBEAT: return OCPP20Constants.OCPP_RESPONSE_EMPTY as unknown as Request + case OCPP20RequestCommand.NOTIFY_REPORT: + return { + generatedAt: new Date(), + ...commandParams, + } as unknown as Request case OCPP20RequestCommand.STATUS_NOTIFICATION: return { timestamp: new Date(), diff --git a/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts index 93d03cc9..c092059f 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts @@ -10,8 +10,10 @@ import { type JsonType, type OCPP20BootNotificationResponse, type OCPP20ClearCacheResponse, + type OCPP20GetBaseReportResponse, type OCPP20HeartbeatResponse, OCPP20IncomingRequestCommand, + type OCPP20NotifyReportResponse, OCPP20OptionalVariableName, OCPP20RequestCommand, type OCPP20StatusNotificationResponse, @@ -45,6 +47,7 @@ export class OCPP20ResponseService extends OCPPResponseService { this.handleResponseBootNotification.bind(this) as ResponseHandler, ], [OCPP20RequestCommand.HEARTBEAT, this.emptyResponseHandler], + [OCPP20RequestCommand.NOTIFY_REPORT, this.emptyResponseHandler], [OCPP20RequestCommand.STATUS_NOTIFICATION, this.emptyResponseHandler], ]) this.payloadValidateFunctions = new Map>([ @@ -68,6 +71,16 @@ export class OCPP20ResponseService extends OCPPResponseService { ) ), ], + [ + OCPP20RequestCommand.NOTIFY_REPORT, + this.ajv.compile( + OCPP20ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/2.0/NotifyReportResponse.json', + moduleName, + 'constructor' + ) + ), + ], [ OCPP20RequestCommand.STATUS_NOTIFICATION, this.ajv.compile( @@ -93,6 +106,16 @@ export class OCPP20ResponseService extends OCPPResponseService { ) ), ], + [ + OCPP20IncomingRequestCommand.GET_BASE_REPORT, + this.ajvIncomingRequest.compile( + OCPP20ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/2.0/GetBaseReportResponse.json', + moduleName, + 'constructor' + ) + ), + ], ]) this.validatePayload = this.validatePayload.bind(this) } diff --git a/src/types/index.ts b/src/types/index.ts index d603d0ad..64450bb5 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -141,19 +141,34 @@ export { type OCPP16StopTransactionRequest, type OCPP16StopTransactionResponse, } from './ocpp/1.6/Transaction.js' -export { BootReasonEnumType, OCPP20ConnectorStatusEnumType } from './ocpp/2.0/Common.js' export { + BootReasonEnumType, + GenericDeviceModelStatusEnumType, + OCPP20ConnectorStatusEnumType, + ReportBaseEnumType, +} from './ocpp/2.0/Common.js' +export { + type ComponentType, + type EVSEType, type OCPP20BootNotificationRequest, type OCPP20ClearCacheRequest, + type OCPP20GetBaseReportRequest, type OCPP20HeartbeatRequest, OCPP20IncomingRequestCommand, + type OCPP20NotifyReportRequest, OCPP20RequestCommand, type OCPP20StatusNotificationRequest, + type ReportDataType, + type VariableAttributeType, + type VariableCharacteristicsType, + type VariableType, } from './ocpp/2.0/Requests.js' export type { OCPP20BootNotificationResponse, OCPP20ClearCacheResponse, + OCPP20GetBaseReportResponse, OCPP20HeartbeatResponse, + OCPP20NotifyReportResponse, OCPP20StatusNotificationResponse, } from './ocpp/2.0/Responses.js' export { OCPP20OptionalVariableName } from './ocpp/2.0/Variables.js' diff --git a/src/types/ocpp/2.0/Common.ts b/src/types/ocpp/2.0/Common.ts index 1bd577be..d1ed68ed 100644 --- a/src/types/ocpp/2.0/Common.ts +++ b/src/types/ocpp/2.0/Common.ts @@ -53,6 +53,13 @@ export enum GetCertificateStatusEnumType { Failed = 'Failed', } +export enum GenericDeviceModelStatusEnumType { + Accepted = 'Accepted', + EmptyResultSet = 'EmptyResultSet', + NotSupported = 'NotSupported', + Rejected = 'Rejected', +} + export enum GetInstalledCertificateStatusEnumType { Accepted = 'Accepted', NotFound = 'NotFound', @@ -90,6 +97,12 @@ export enum OperationalStatusEnumType { Operative = 'Operative', } +export enum ReportBaseEnumType { + ConfigurationInventory = 'ConfigurationInventory', + FullInventory = 'FullInventory', + SummaryInventory = 'SummaryInventory', +} + export interface CertificateHashDataChainType extends JsonObject { certificateHashData: CertificateHashDataType certificateType: GetCertificateIdUseEnumType diff --git a/src/types/ocpp/2.0/Requests.ts b/src/types/ocpp/2.0/Requests.ts index 0cf30f92..1c2fb339 100644 --- a/src/types/ocpp/2.0/Requests.ts +++ b/src/types/ocpp/2.0/Requests.ts @@ -4,11 +4,13 @@ import type { BootReasonEnumType, InstallCertificateUseEnumType, OCPP20ConnectorStatusEnumType, + ReportBaseEnumType, } from './Common.js' import type { OCPP20SetVariableDataType } from './Variables.js' export enum OCPP20IncomingRequestCommand { CLEAR_CACHE = 'ClearCache', + GET_BASE_REPORT = 'GetBaseReport', REQUEST_START_TRANSACTION = 'RequestStartTransaction', REQUEST_STOP_TRANSACTION = 'RequestStopTransaction', } @@ -16,6 +18,7 @@ export enum OCPP20IncomingRequestCommand { export enum OCPP20RequestCommand { BOOT_NOTIFICATION = 'BootNotification', HEARTBEAT = 'Heartbeat', + NOTIFY_REPORT = 'NotifyReport', STATUS_NOTIFICATION = 'StatusNotification', } @@ -26,8 +29,21 @@ export interface OCPP20BootNotificationRequest extends JsonObject { export type OCPP20ClearCacheRequest = EmptyObject +export interface OCPP20GetBaseReportRequest extends JsonObject { + reportBase: ReportBaseEnumType + requestId: number +} + export type OCPP20HeartbeatRequest = EmptyObject +export interface OCPP20NotifyReportRequest extends JsonObject { + generatedAt: Date + requestId: number + reportData?: ReportDataType[] + seqNo: number + tbc?: boolean +} + export interface OCPP20InstallCertificateRequest extends JsonObject { certificate: string certificateType: InstallCertificateUseEnumType @@ -56,3 +72,36 @@ interface ModemType extends JsonObject { iccid?: string imsi?: string } + +export interface ReportDataType extends JsonObject { + component: ComponentType + variable: VariableType + variableAttribute?: VariableAttributeType[] + variableCharacteristics?: VariableCharacteristicsType +} + +export interface ComponentType extends JsonObject { + evse?: EVSEType + instance?: string + name: string +} + +export interface VariableType extends JsonObject { + instance?: string + name: string +} + +export interface VariableAttributeType extends JsonObject { + type?: string + value?: string +} + +export interface VariableCharacteristicsType extends JsonObject { + dataType: string + supportsMonitoring: boolean +} + +export interface EVSEType extends JsonObject { + connectorId?: number + id: number +} diff --git a/src/types/ocpp/2.0/Responses.ts b/src/types/ocpp/2.0/Responses.ts index e735c788..225c5aae 100644 --- a/src/types/ocpp/2.0/Responses.ts +++ b/src/types/ocpp/2.0/Responses.ts @@ -2,6 +2,7 @@ import type { EmptyObject } from '../../EmptyObject.js' import type { JsonObject } from '../../JsonType.js' import type { RegistrationStatusEnumType } from '../Common.js' import type { + GenericDeviceModelStatusEnumType, GenericStatusEnumType, InstallCertificateStatusEnumType, StatusInfoType, @@ -20,10 +21,17 @@ export interface OCPP20ClearCacheResponse extends JsonObject { statusInfo?: StatusInfoType } +export interface OCPP20GetBaseReportResponse extends JsonObject { + status: GenericDeviceModelStatusEnumType + statusInfo?: StatusInfoType +} + export interface OCPP20HeartbeatResponse extends JsonObject { currentTime: Date } +export type OCPP20NotifyReportResponse = EmptyObject + export interface OCPP20InstallCertificateResponse extends JsonObject { status: InstallCertificateStatusEnumType statusInfo?: StatusInfoType diff --git a/tests/ocpp-server/server.py b/tests/ocpp-server/server.py index 9dce9e1b..6a772e0a 100644 --- a/tests/ocpp-server/server.py +++ b/tests/ocpp-server/server.py @@ -96,6 +96,13 @@ class ChargePoint(ocpp.v201.ChargePoint): logging.info("Received %s", Action.meter_values) return ocpp.v201.call_result.MeterValues() + @on(Action.notify_report) + async def on_notify_report( + self, request_id: int, generated_at, seq_no: int, **kwargs + ): + logging.info("Received %s", Action.notify_report) + return ocpp.v201.call_result.NotifyReport() + # Request handlers to emit OCPP messages. async def _send_clear_cache(self): request = ocpp.v201.call.ClearCache() -- 2.43.0