// Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
+import { randomInt } from 'node:crypto'
import { createWriteStream, readdirSync } from 'node:fs'
-import { dirname, join, resolve } from 'node:path'
-import { URL, fileURLToPath } from 'node:url'
+import { dirname, extname, join, resolve } from 'node:path'
+import { fileURLToPath, URL } from 'node:url'
import type { ValidateFunction } from 'ajv'
import { Client, type FTPResponse } from 'basic-ftp'
import {
- type Interval,
addSeconds,
differenceInSeconds,
+ type Interval,
isDate,
secondsToMilliseconds
} from 'date-fns'
import { maxTime } from 'date-fns/constants'
+import { isEmpty } from 'rambda'
import { create } from 'tar'
-import { OCPP16Constants } from './OCPP16Constants.js'
-import { OCPP16ServiceUtils } from './OCPP16ServiceUtils.js'
import {
- type ChargingStation,
canProceedChargingProfile,
+ type ChargingStation,
checkChargingStation,
getConfigurationKey,
getConnectorChargingProfiles,
import {
type ChangeConfigurationRequest,
type ChangeConfigurationResponse,
+ ConfigurationSection,
ErrorType,
type GenericResponse,
GenericStatus,
type GetDiagnosticsResponse,
type IncomingRequestHandler,
type JsonType,
+ type LogConfiguration,
OCPP16AuthorizationStatus,
OCPP16AvailabilityType,
type OCPP16BootNotificationRequest,
OCPP16SupportedFeatureProfiles,
type OCPP16TriggerMessageRequest,
type OCPP16TriggerMessageResponse,
+ OCPP16TriggerMessageStatus,
type OCPP16UpdateFirmwareRequest,
type OCPP16UpdateFirmwareResponse,
type OCPPConfigurationKey,
type UnlockConnectorResponse
} from '../../../types/index.js'
import {
+ Configuration,
Constants,
convertToDate,
convertToInt,
formatDurationMilliSeconds,
- getRandomInteger,
- isEmptyArray,
+ isAsyncFunction,
isNotEmptyArray,
isNotEmptyString,
logger,
sleep
} from '../../../utils/index.js'
import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService.js'
+import { OCPP16Constants } from './OCPP16Constants.js'
+import { OCPP16ServiceUtils } from './OCPP16ServiceUtils.js'
const moduleName = 'OCPP16IncomingRequestService'
.bind(this)
]
])
+ // Handle incoming request events
+ this.on(
+ OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
+ (
+ chargingStation: ChargingStation,
+ request: RemoteStartTransactionRequest,
+ response: GenericResponse
+ ) => {
+ if (response.status === GenericStatus.Accepted) {
+ const { connectorId, idTag } = request
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ chargingStation.getConnectorStatus(connectorId)!.transactionRemoteStarted = true
+ chargingStation.ocppRequestService
+ .requestHandler<OCPP16StartTransactionRequest, OCPP16StartTransactionResponse>(
+ chargingStation,
+ OCPP16RequestCommand.START_TRANSACTION,
+ {
+ connectorId,
+ idTag
+ }
+ )
+ .then(response => {
+ if (response.status === OCPP16AuthorizationStatus.ACCEPTED) {
+ logger.debug(
+ `${chargingStation.logPrefix()} Remote start transaction ACCEPTED on ${chargingStation.stationInfo?.chargingStationId}#${connectorId} for idTag '${idTag}'`
+ )
+ } else {
+ logger.debug(
+ `${chargingStation.logPrefix()} Remote start transaction REJECTED on ${chargingStation.stationInfo?.chargingStationId}#${connectorId} for idTag '${idTag}'`
+ )
+ }
+ })
+ .catch((error: unknown) => {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.constructor: Remote start transaction error:`,
+ error
+ )
+ })
+ }
+ }
+ )
+ this.on(
+ OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION,
+ (
+ chargingStation: ChargingStation,
+ request: RemoteStopTransactionRequest,
+ response: GenericResponse
+ ) => {
+ if (response.status === GenericStatus.Accepted) {
+ const { transactionId } = request
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const connectorId = chargingStation.getConnectorIdByTransactionId(transactionId)!
+ OCPP16ServiceUtils.remoteStopTransaction(chargingStation, connectorId)
+ .then(response => {
+ if (response.status === GenericStatus.Accepted) {
+ logger.debug(
+ `${chargingStation.logPrefix()} Remote stop transaction ACCEPTED on ${chargingStation.stationInfo?.chargingStationId}#${connectorId} for transaction '${transactionId}'`
+ )
+ } else {
+ logger.debug(
+ `${chargingStation.logPrefix()} Remote stop transaction REJECTED on ${chargingStation.stationInfo?.chargingStationId}#${connectorId} for transaction '${transactionId}'`
+ )
+ }
+ })
+ .catch((error: unknown) => {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.constructor: Remote stop transaction error:`,
+ error
+ )
+ })
+ }
+ }
+ )
+ this.on(
+ OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
+ (
+ chargingStation: ChargingStation,
+ request: OCPP16TriggerMessageRequest,
+ response: OCPP16TriggerMessageResponse
+ ) => {
+ if (response.status !== OCPP16TriggerMessageStatus.ACCEPTED) {
+ return
+ }
+ const { requestedMessage, connectorId } = request
+ const errorHandler = (error: unknown): void => {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.constructor: Trigger ${requestedMessage} error:`,
+ error
+ )
+ }
+ switch (requestedMessage) {
+ case OCPP16MessageTrigger.BootNotification:
+ chargingStation.ocppRequestService
+ .requestHandler<OCPP16BootNotificationRequest, OCPP16BootNotificationResponse>(
+ chargingStation,
+ OCPP16RequestCommand.BOOT_NOTIFICATION,
+ chargingStation.bootNotificationRequest,
+ { skipBufferingOnError: true, triggerMessage: true }
+ )
+ .then(response => {
+ chargingStation.bootNotificationResponse = response
+ })
+ .catch(errorHandler)
+ break
+ case OCPP16MessageTrigger.Heartbeat:
+ chargingStation.ocppRequestService
+ .requestHandler<OCPP16HeartbeatRequest, OCPP16HeartbeatResponse>(
+ chargingStation,
+ OCPP16RequestCommand.HEARTBEAT,
+ undefined,
+ {
+ triggerMessage: true
+ }
+ )
+ .catch(errorHandler)
+ break
+ case OCPP16MessageTrigger.StatusNotification:
+ if (connectorId != null) {
+ chargingStation.ocppRequestService
+ .requestHandler<OCPP16StatusNotificationRequest, OCPP16StatusNotificationResponse>(
+ chargingStation,
+ OCPP16RequestCommand.STATUS_NOTIFICATION,
+ {
+ connectorId,
+ errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
+ status: chargingStation.getConnectorStatus(connectorId)?.status
+ },
+ {
+ triggerMessage: true
+ }
+ )
+ .catch(errorHandler)
+ } else if (chargingStation.hasEvses) {
+ for (const evseStatus of chargingStation.evses.values()) {
+ for (const [id, connectorStatus] of evseStatus.connectors) {
+ chargingStation.ocppRequestService
+ .requestHandler<
+ OCPP16StatusNotificationRequest,
+ OCPP16StatusNotificationResponse
+ >(
+ chargingStation,
+ OCPP16RequestCommand.STATUS_NOTIFICATION,
+ {
+ connectorId: id,
+ errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
+ status: connectorStatus.status
+ },
+ {
+ triggerMessage: true
+ }
+ )
+ .catch(errorHandler)
+ }
+ }
+ } else {
+ for (const [id, connectorStatus] of chargingStation.connectors) {
+ chargingStation.ocppRequestService
+ .requestHandler<
+ OCPP16StatusNotificationRequest,
+ OCPP16StatusNotificationResponse
+ >(
+ chargingStation,
+ OCPP16RequestCommand.STATUS_NOTIFICATION,
+ {
+ connectorId: id,
+ errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
+ status: connectorStatus.status
+ },
+ {
+ triggerMessage: true
+ }
+ )
+ .catch(errorHandler)
+ }
+ }
+ break
+ }
+ }
+ )
this.validatePayload = this.validatePayload.bind(this)
}
this.validatePayload(chargingStation, commandName, commandPayload)
// Call the method to build the response
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- response = (await this.incomingRequestHandlers.get(commandName)!(
- chargingStation,
- commandPayload
- )) as ResType
+ const incomingRequestHandler = this.incomingRequestHandlers.get(commandName)!
+ if (isAsyncFunction(incomingRequestHandler)) {
+ response = (await incomingRequestHandler(chargingStation, commandPayload)) as ResType
+ } else {
+ response = incomingRequestHandler(chargingStation, commandPayload) as ResType
+ }
} catch (error) {
// Log
logger.error(
// Throw exception
throw new OCPPError(
ErrorType.NOT_IMPLEMENTED,
- `${commandName} is not implemented to handle request PDU ${JSON.stringify(
+ `'${commandName}' is not implemented to handle request PDU ${JSON.stringify(
commandPayload,
undefined,
2
commandPayload,
undefined,
2
- )} while the charging station is not registered on the central server.`,
+ )} while the charging station is not registered on the central server`,
commandName,
commandPayload
)
response,
commandName
)
+ // Emit command name event to allow delayed handling
+ this.emit(commandName, chargingStation, commandPayload, response)
}
private validatePayload (
}
const connectorStatus = chargingStation.getConnectorStatus(connectorId)
if (
- isEmptyArray(connectorStatus?.chargingProfiles) &&
- isEmptyArray(chargingStation.getConnectorStatus(0)?.chargingProfiles)
+ isEmpty(connectorStatus?.chargingProfiles) &&
+ isEmpty(chargingStation.getConnectorStatus(0)?.chargingProfiles)
) {
return OCPP16Constants.OCPP_RESPONSE_REJECTED
}
idTag
)
}
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- chargingStation.getConnectorStatus(transactionConnectorId)!.transactionRemoteStarted = true
- if (
- (
- await chargingStation.ocppRequestService.requestHandler<
- OCPP16StartTransactionRequest,
- OCPP16StartTransactionResponse
- >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, {
- connectorId: transactionConnectorId,
- idTag
- })
- ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED
- ) {
- logger.debug(`
- ${chargingStation.logPrefix()} Transaction remotely STARTED on ${
- chargingStation.stationInfo?.chargingStationId
- }#${transactionConnectorId} for idTag '${idTag}'`)
- return OCPP16Constants.OCPP_RESPONSE_ACCEPTED
- }
- return await this.notifyRemoteStartTransactionRejected(
- chargingStation,
- transactionConnectorId,
- idTag
+ logger.debug(
+ `${chargingStation.logPrefix()} Remote start transaction ACCEPTED on connector id ${transactionConnectorId}, idTag '${idTag}'`
)
+ return OCPP16Constants.OCPP_RESPONSE_ACCEPTED
}
private async notifyRemoteStartTransactionRejected (
OCPP16ChargePointStatus.Available
)
}
- logger.warn(
- `${chargingStation.logPrefix()} Remote starting transaction REJECTED on connector id ${connectorId}, idTag '${idTag}', availability '${connectorStatus?.availability}', status '${connectorStatus?.status}'`
+ logger.debug(
+ `${chargingStation.logPrefix()} Remote start transaction REJECTED on connector id ${connectorId}, idTag '${idTag}', availability '${connectorStatus?.availability}', status '${connectorStatus?.status}'`
)
return OCPP16Constants.OCPP_RESPONSE_REJECTED
}
)
return true
}
- logger.warn(
+ logger.debug(
`${chargingStation.logPrefix()} Not allowed to set ${
chargingProfile.chargingProfilePurpose
} charging profile(s) at remote start transaction`
return false
}
- private async handleRequestRemoteStopTransaction (
+ private handleRequestRemoteStopTransaction (
chargingStation: ChargingStation,
commandPayload: RemoteStopTransactionRequest
- ): Promise<GenericResponse> {
+ ): GenericResponse {
const { transactionId } = commandPayload
- if (chargingStation.hasEvses) {
- for (const [evseId, evseStatus] of chargingStation.evses) {
- if (evseId > 0) {
- for (const [connectorId, connectorStatus] of evseStatus.connectors) {
- if (connectorStatus.transactionId === transactionId) {
- return await OCPP16ServiceUtils.remoteStopTransaction(chargingStation, connectorId)
- }
- }
- }
- }
- } else {
- for (const connectorId of chargingStation.connectors.keys()) {
- if (
- connectorId > 0 &&
- chargingStation.getConnectorStatus(connectorId)?.transactionId === transactionId
- ) {
- return await OCPP16ServiceUtils.remoteStopTransaction(chargingStation, connectorId)
- }
- }
+ if (chargingStation.getConnectorIdByTransactionId(transactionId) != null) {
+ logger.debug(
+ `${chargingStation.logPrefix()} Remote stop transaction ACCEPTED for transactionId '${transactionId}'`
+ )
+ return OCPP16Constants.OCPP_RESPONSE_ACCEPTED
}
- logger.warn(
- `${chargingStation.logPrefix()} Trying to remote stop a non existing transaction with id ${transactionId}`
+ logger.debug(
+ `${chargingStation.logPrefix()} Remote stop transaction REJECTED for transactionId '${transactionId}'`
)
return OCPP16Constants.OCPP_RESPONSE_REJECTED
}
chargingStation.stationInfo?.firmwareUpgrade?.failureStatus ===
OCPP16FirmwareStatus.DownloadFailed
) {
- await sleep(secondsToMilliseconds(getRandomInteger(maxDelay, minDelay)))
+ await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
await chargingStation.ocppRequestService.requestHandler<
OCPP16FirmwareStatusNotificationRequest,
OCPP16FirmwareStatusNotificationResponse
chargingStation.stationInfo.firmwareUpgrade.failureStatus
return
}
- await sleep(secondsToMilliseconds(getRandomInteger(maxDelay, minDelay)))
+ await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
await chargingStation.ocppRequestService.requestHandler<
OCPP16FirmwareStatusNotificationRequest,
OCPP16FirmwareStatusNotificationResponse
transactionsStarted = false
}
} while (transactionsStarted)
- !wasTransactionsStarted &&
- (await sleep(secondsToMilliseconds(getRandomInteger(maxDelay, minDelay))))
+ !wasTransactionsStarted && (await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay))))
if (!checkChargingStation(chargingStation, chargingStation.logPrefix())) {
return
}
chargingStation.stationInfo?.firmwareUpgrade?.failureStatus ===
OCPP16FirmwareStatus.InstallationFailed
) {
- await sleep(secondsToMilliseconds(getRandomInteger(maxDelay, minDelay)))
+ await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
await chargingStation.ocppRequestService.requestHandler<
OCPP16FirmwareStatusNotificationRequest,
OCPP16FirmwareStatusNotificationResponse
return
}
if (chargingStation.stationInfo?.firmwareUpgrade?.reset === true) {
- await sleep(secondsToMilliseconds(getRandomInteger(maxDelay, minDelay)))
+ await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
await chargingStation.reset(OCPP16StopTransactionReason.REBOOT)
}
}
if (uri.protocol.startsWith('ftp:')) {
let ftpClient: Client | undefined
try {
+ const logConfiguration = Configuration.getConfigurationSection<LogConfiguration>(
+ ConfigurationSection.log
+ )
const logFiles = readdirSync(resolve(dirname(fileURLToPath(import.meta.url)), '../'))
- .filter(file => file.endsWith('.log'))
- .map(file => join('./', file))
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ .filter(file => file.endsWith(extname(logConfiguration.file!)))
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ .map(file => join(dirname(logConfiguration.file!), file))
const diagnosticsArchive = `${chargingStation.stationInfo?.chargingStationId}_logs.tar.gz`
create({ gzip: true }, logFiles).pipe(createWriteStream(diagnosticsArchive))
ftpClient = new Client()
const accessResponse = await ftpClient.access({
- host: uri.host,
+ host: uri.hostname,
...(isNotEmptyString(uri.port) && { port: convertToInt(uri.port) }),
...(isNotEmptyString(uri.username) && { user: uri.username }),
...(isNotEmptyString(uri.password) && { password: uri.password })
>(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
status: OCPP16DiagnosticsStatus.Uploading
})
- .catch(error => {
+ .catch((error: unknown) => {
logger.error(
`${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: Error while sending '${
OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION
) {
return OCPP16Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_REJECTED
}
- try {
- switch (requestedMessage) {
- case OCPP16MessageTrigger.BootNotification:
- setTimeout(() => {
- chargingStation.ocppRequestService
- .requestHandler<OCPP16BootNotificationRequest, OCPP16BootNotificationResponse>(
- chargingStation,
- OCPP16RequestCommand.BOOT_NOTIFICATION,
- chargingStation.bootNotificationRequest,
- { skipBufferingOnError: true, triggerMessage: true }
- )
- .then(response => {
- chargingStation.bootNotificationResponse = response
- })
- .catch(Constants.EMPTY_FUNCTION)
- }, OCPP16Constants.OCPP_TRIGGER_MESSAGE_DELAY)
- return OCPP16Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED
- case OCPP16MessageTrigger.Heartbeat:
- setTimeout(() => {
- chargingStation.ocppRequestService
- .requestHandler<OCPP16HeartbeatRequest, OCPP16HeartbeatResponse>(
- chargingStation,
- OCPP16RequestCommand.HEARTBEAT,
- undefined,
- {
- triggerMessage: true
- }
- )
- .catch(Constants.EMPTY_FUNCTION)
- }, OCPP16Constants.OCPP_TRIGGER_MESSAGE_DELAY)
- return OCPP16Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED
- case OCPP16MessageTrigger.StatusNotification:
- setTimeout(() => {
- if (connectorId != null) {
- chargingStation.ocppRequestService
- .requestHandler<OCPP16StatusNotificationRequest, OCPP16StatusNotificationResponse>(
- chargingStation,
- OCPP16RequestCommand.STATUS_NOTIFICATION,
- {
- connectorId,
- errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
- status: chargingStation.getConnectorStatus(connectorId)?.status
- },
- {
- triggerMessage: true
- }
- )
- .catch(Constants.EMPTY_FUNCTION)
- } else if (chargingStation.hasEvses) {
- for (const evseStatus of chargingStation.evses.values()) {
- for (const [id, connectorStatus] of evseStatus.connectors) {
- chargingStation.ocppRequestService
- .requestHandler<
- OCPP16StatusNotificationRequest,
- OCPP16StatusNotificationResponse
- >(
- chargingStation,
- OCPP16RequestCommand.STATUS_NOTIFICATION,
- {
- connectorId: id,
- errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
- status: connectorStatus.status
- },
- {
- triggerMessage: true
- }
- )
- .catch(Constants.EMPTY_FUNCTION)
- }
- }
- } else {
- for (const [id, connectorStatus] of chargingStation.connectors) {
- chargingStation.ocppRequestService
- .requestHandler<
- OCPP16StatusNotificationRequest,
- OCPP16StatusNotificationResponse
- >(
- chargingStation,
- OCPP16RequestCommand.STATUS_NOTIFICATION,
- {
- connectorId: id,
- errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
- status: connectorStatus.status
- },
- {
- triggerMessage: true
- }
- )
- .catch(Constants.EMPTY_FUNCTION)
- }
- }
- }, OCPP16Constants.OCPP_TRIGGER_MESSAGE_DELAY)
- return OCPP16Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED
- default:
- return OCPP16Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_NOT_IMPLEMENTED
- }
- } catch (error) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return this.handleIncomingRequestError<OCPP16TriggerMessageResponse>(
- chargingStation,
- OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
- error as Error,
- { errorResponse: OCPP16Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_REJECTED }
- )!
+ switch (requestedMessage) {
+ case OCPP16MessageTrigger.BootNotification:
+ case OCPP16MessageTrigger.Heartbeat:
+ case OCPP16MessageTrigger.StatusNotification:
+ return OCPP16Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED
+ default:
+ return OCPP16Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_NOT_IMPLEMENTED
}
}