X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2Focpp%2FOCPPServiceUtils.ts;h=d9d712981c816790cb4e2b6f1c09fa6cd10e7dac;hb=1b2acf4e9c00cc7272ec7769b4e82113d61f64fb;hp=f7a5d61ceb772f23d4da6b7fc5e394848ca52d8f;hpb=b30ea3f0e52c291362c3b0fc5dfa13c2696b9912;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ocpp/OCPPServiceUtils.ts b/src/charging-station/ocpp/OCPPServiceUtils.ts index f7a5d61c..d9d71298 100644 --- a/src/charging-station/ocpp/OCPPServiceUtils.ts +++ b/src/charging-station/ocpp/OCPPServiceUtils.ts @@ -1,16 +1,26 @@ -import fs from 'node:fs'; +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; import type { DefinedError, ErrorObject, JSONSchemaType } from 'ajv'; +import { isDate } from 'date-fns'; -import { type ChargingStation, ChargingStationConfigurationUtils } from '../../charging-station'; +import { OCPP16Constants } from './1.6/OCPP16Constants'; +import { OCPP20Constants } from './2.0/OCPP20Constants'; +import { OCPPConstants } from './OCPPConstants'; +import { type ChargingStation, getConfigurationKey, getIdTagsFile } from '../../charging-station'; import { BaseError } from '../../exception'; import { + AuthorizationStatus, + type AuthorizeRequest, + type AuthorizeResponse, ChargePointErrorCode, + ChargingStationEvents, + type ConnectorStatus, type ConnectorStatusEnum, ErrorType, FileType, IncomingRequestCommand, - type JsonObject, type JsonType, MessageTrigger, MessageType, @@ -23,25 +33,36 @@ import { type SampledValueTemplate, StandardParametersKey, type StatusNotificationRequest, + type StatusNotificationResponse, } from '../../types'; -import { Constants, FileUtils, Utils, logger } from '../../utils'; +import { + handleFileException, + isNotEmptyArray, + isNotEmptyString, + logPrefix, + logger, + max, + min, +} from '../../utils'; export class OCPPServiceUtils { protected constructor() { // This is intentional } - public static ajvErrorsToErrorType(errors: ErrorObject[]): ErrorType { - for (const error of errors as DefinedError[]) { - switch (error.keyword) { - case 'type': - return ErrorType.TYPE_CONSTRAINT_VIOLATION; - case 'dependencies': - case 'required': - return ErrorType.OCCURRENCE_CONSTRAINT_VIOLATION; - case 'pattern': - case 'format': - return ErrorType.PROPERTY_CONSTRAINT_VIOLATION; + public static ajvErrorsToErrorType(errors: ErrorObject[] | null | undefined): ErrorType { + if (isNotEmptyArray(errors) === true) { + for (const error of errors as DefinedError[]) { + switch (error.keyword) { + case 'type': + return ErrorType.TYPE_CONSTRAINT_VIOLATION; + case 'dependencies': + case 'required': + return ErrorType.OCCURRENCE_CONSTRAINT_VIOLATION; + case 'pattern': + case 'format': + return ErrorType.PROPERTY_CONSTRAINT_VIOLATION; + } } } return ErrorType.FORMAT_VIOLATION; @@ -62,7 +83,7 @@ export class OCPPServiceUtils { public static isRequestCommandSupported( chargingStation: ChargingStation, - command: RequestCommand + command: RequestCommand, ): boolean { const isRequestCommand = Object.values(RequestCommand).includes(command); if ( @@ -72,9 +93,9 @@ export class OCPPServiceUtils { return true; } else if ( isRequestCommand === true && - chargingStation.stationInfo?.commandsSupport?.outgoingCommands + chargingStation.stationInfo?.commandsSupport?.outgoingCommands?.[command] ) { - return chargingStation.stationInfo?.commandsSupport?.outgoingCommands[command] ?? false; + return chargingStation.stationInfo?.commandsSupport?.outgoingCommands[command]; } logger.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`); return false; @@ -82,7 +103,7 @@ export class OCPPServiceUtils { public static isIncomingRequestCommandSupported( chargingStation: ChargingStation, - command: IncomingRequestCommand + command: IncomingRequestCommand, ): boolean { const isIncomingRequestCommand = Object.values(IncomingRequestCommand).includes(command); @@ -93,9 +114,9 @@ export class OCPPServiceUtils { return true; } else if ( isIncomingRequestCommand === true && - chargingStation.stationInfo?.commandsSupport?.incomingCommands + chargingStation.stationInfo?.commandsSupport?.incomingCommands?.[command] ) { - return chargingStation.stationInfo?.commandsSupport?.incomingCommands[command] ?? false; + return chargingStation.stationInfo?.commandsSupport?.incomingCommands[command]; } logger.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`); return false; @@ -103,16 +124,19 @@ export class OCPPServiceUtils { public static isMessageTriggerSupported( chargingStation: ChargingStation, - messageTrigger: MessageTrigger + messageTrigger: MessageTrigger, ): boolean { const isMessageTrigger = Object.values(MessageTrigger).includes(messageTrigger); if (isMessageTrigger === true && !chargingStation.stationInfo?.messageTriggerSupport) { return true; - } else if (isMessageTrigger === true && chargingStation.stationInfo?.messageTriggerSupport) { - return chargingStation.stationInfo?.messageTriggerSupport[messageTrigger] ?? false; + } else if ( + isMessageTrigger === true && + chargingStation.stationInfo?.messageTriggerSupport?.[messageTrigger] + ) { + return chargingStation.stationInfo?.messageTriggerSupport[messageTrigger]; } logger.error( - `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'` + `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`, ); return false; } @@ -120,11 +144,11 @@ export class OCPPServiceUtils { public static isConnectorIdValid( chargingStation: ChargingStation, ocppCommand: IncomingRequestCommand, - connectorId: number + connectorId: number, ): boolean { if (connectorId < 0) { logger.error( - `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector Id ${connectorId}` + `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`, ); return false; } @@ -133,10 +157,14 @@ export class OCPPServiceUtils { public static convertDateToISOString(obj: T): void { for (const key in obj) { - if (obj[key] instanceof Date) { - (obj as JsonObject)[key] = (obj[key] as Date).toISOString(); - } else if (obj[key] !== null && typeof obj[key] === 'object') { - OCPPServiceUtils.convertDateToISOString(obj[key] as T); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + if (isDate(obj![key])) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + (obj![key] as string) = (obj![key] as Date).toISOString(); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + } else if (obj![key] !== null && typeof obj![key] === 'object') { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + OCPPServiceUtils.convertDateToISOString(obj![key] as T); } } } @@ -144,9 +172,10 @@ export class OCPPServiceUtils { public static buildStatusNotificationRequest( chargingStation: ChargingStation, connectorId: number, - status: ConnectorStatusEnum + status: ConnectorStatusEnum, + evseId?: number, ): StatusNotificationRequest { - switch (chargingStation.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16) { + switch (chargingStation.stationInfo?.ocppVersion) { case OCPPVersion.VERSION_16: return { connectorId, @@ -159,7 +188,7 @@ export class OCPPServiceUtils { timestamp: new Date(), connectorStatus: status, connectorId, - evseId: connectorId, + evseId, } as OCPP20StatusNotificationRequest; default: throw new BaseError('Cannot build status notification payload: OCPP version not supported'); @@ -174,22 +203,136 @@ export class OCPPServiceUtils { } } + public static async sendAndSetConnectorStatus( + chargingStation: ChargingStation, + connectorId: number, + status: ConnectorStatusEnum, + evseId?: number, + options?: { send: boolean }, + ) { + options = { send: true, ...options }; + if (options.send) { + OCPPServiceUtils.checkConnectorStatusTransition(chargingStation, connectorId, status); + await chargingStation.ocppRequestService.requestHandler< + StatusNotificationRequest, + StatusNotificationResponse + >( + chargingStation, + RequestCommand.STATUS_NOTIFICATION, + OCPPServiceUtils.buildStatusNotificationRequest( + chargingStation, + connectorId, + status, + evseId, + ), + ); + } + chargingStation.getConnectorStatus(connectorId)!.status = status; + chargingStation.emit(ChargingStationEvents.connectorStatusChanged, { + connectorId, + ...chargingStation.getConnectorStatus(connectorId), + }); + } + + public static async isIdTagAuthorized( + chargingStation: ChargingStation, + connectorId: number, + idTag: string, + ): Promise { + if ( + !chargingStation.getLocalAuthListEnabled() && + !chargingStation.stationInfo?.remoteAuthorization + ) { + logger.warn( + `${chargingStation.logPrefix()} The charging station expects to authorize RFID tags but nor local authorization nor remote authorization are enabled. Misbehavior may occur`, + ); + } + if ( + chargingStation.getLocalAuthListEnabled() === true && + OCPPServiceUtils.isIdTagLocalAuthorized(chargingStation, idTag) + ) { + const connectorStatus: ConnectorStatus = chargingStation.getConnectorStatus(connectorId)!; + connectorStatus.localAuthorizeIdTag = idTag; + connectorStatus.idTagLocalAuthorized = true; + return true; + } else if (chargingStation.stationInfo?.remoteAuthorization) { + return await OCPPServiceUtils.isIdTagRemoteAuthorized(chargingStation, connectorId, idTag); + } + return false; + } + + protected static checkConnectorStatusTransition( + chargingStation: ChargingStation, + connectorId: number, + status: ConnectorStatusEnum, + ): boolean { + const fromStatus = chargingStation.getConnectorStatus(connectorId)!.status; + let transitionAllowed = false; + switch (chargingStation.stationInfo?.ocppVersion) { + case OCPPVersion.VERSION_16: + if ( + (connectorId === 0 && + OCPP16Constants.ChargePointStatusChargingStationTransitions.findIndex( + (transition) => transition.from === fromStatus && transition.to === status, + ) !== -1) || + (connectorId > 0 && + OCPP16Constants.ChargePointStatusConnectorTransitions.findIndex( + (transition) => transition.from === fromStatus && transition.to === status, + ) !== -1) + ) { + transitionAllowed = true; + } + break; + case OCPPVersion.VERSION_20: + case OCPPVersion.VERSION_201: + if ( + (connectorId === 0 && + OCPP20Constants.ChargingStationStatusTransitions.findIndex( + (transition) => transition.from === fromStatus && transition.to === status, + ) !== -1) || + (connectorId > 0 && + OCPP20Constants.ConnectorStatusTransitions.findIndex( + (transition) => transition.from === fromStatus && transition.to === status, + ) !== -1) + ) { + transitionAllowed = true; + } + break; + default: + throw new BaseError( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`, + ); + } + if (transitionAllowed === false) { + logger.warn( + `${chargingStation.logPrefix()} OCPP ${chargingStation.stationInfo + ?.ocppVersion} connector id ${connectorId} status transition from '${ + chargingStation.getConnectorStatus(connectorId)!.status + }' to '${status}' is not allowed`, + ); + } + return transitionAllowed; + } + protected static parseJsonSchemaFile( - filePath: string, + relativePath: string, ocppVersion: OCPPVersion, moduleName?: string, - methodName?: string + methodName?: string, ): JSONSchemaType { + const filePath = join(dirname(fileURLToPath(import.meta.url)), relativePath); try { - return JSON.parse(fs.readFileSync(filePath, 'utf8')) as JSONSchemaType; + return JSON.parse(readFileSync(filePath, 'utf8')) as JSONSchemaType; } catch (error) { - FileUtils.handleFileException( + handleFileException( filePath, FileType.JsonSchema, error as NodeJS.ErrnoException, OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName), - { throwError: false } + { throwError: false }, ); + return {} as JSONSchemaType; } } @@ -197,109 +340,145 @@ export class OCPPServiceUtils { chargingStation: ChargingStation, connectorId: number, measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER, - phase?: MeterValuePhase + phase?: MeterValuePhase, ): SampledValueTemplate | undefined { const onPhaseStr = phase ? `on phase ${phase} ` : ''; - if (Constants.SUPPORTED_MEASURANDS.includes(measurand) === false) { + if (OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(measurand) === false) { logger.warn( - `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}` + `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`, ); return; } if ( measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER && - ChargingStationConfigurationUtils.getConfigurationKey( + getConfigurationKey( chargingStation, - StandardParametersKey.MeterValuesSampledData + StandardParametersKey.MeterValuesSampledData, )?.value?.includes(measurand) === false ) { logger.debug( - `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId} not found in '${ + `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${ StandardParametersKey.MeterValuesSampledData - }' OCPP parameter` + }' OCPP parameter`, ); return; } const sampledValueTemplates: SampledValueTemplate[] = - chargingStation.getConnectorStatus(connectorId)?.MeterValues; + chargingStation.getConnectorStatus(connectorId)!.MeterValues; for ( let index = 0; - Utils.isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length; + isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length; index++ ) { if ( - Constants.SUPPORTED_MEASURANDS.includes( + OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes( sampledValueTemplates[index]?.measurand ?? - MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER + MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER, ) === false ) { logger.warn( - `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}` + `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`, ); } else if ( phase && sampledValueTemplates[index]?.phase === phase && sampledValueTemplates[index]?.measurand === measurand && - ChargingStationConfigurationUtils.getConfigurationKey( + getConfigurationKey( chargingStation, - StandardParametersKey.MeterValuesSampledData + StandardParametersKey.MeterValuesSampledData, )?.value?.includes(measurand) === true ) { return sampledValueTemplates[index]; } else if ( !phase && - !sampledValueTemplates[index].phase && + !sampledValueTemplates[index]?.phase && sampledValueTemplates[index]?.measurand === measurand && - ChargingStationConfigurationUtils.getConfigurationKey( + getConfigurationKey( chargingStation, - StandardParametersKey.MeterValuesSampledData + StandardParametersKey.MeterValuesSampledData, )?.value?.includes(measurand) === true ) { return sampledValueTemplates[index]; } else if ( measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER && - (!sampledValueTemplates[index].measurand || - sampledValueTemplates[index].measurand === measurand) + (!sampledValueTemplates[index]?.measurand || + sampledValueTemplates[index]?.measurand === measurand) ) { return sampledValueTemplates[index]; } } if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) { - const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`; + const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`; logger.error(`${chargingStation.logPrefix()} ${errorMsg}`); throw new BaseError(errorMsg); } logger.debug( - `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}` + `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`, ); } protected static getLimitFromSampledValueTemplateCustomValue( - value: string, - limit: number, - options: { limitationEnabled?: boolean; unitMultiplier?: number } = { - limitationEnabled: true, - unitMultiplier: 1, - } + value: string | undefined, + maxLimit: number, + minLimit: number, + options?: { limitationEnabled?: boolean; fallbackValue?: number; unitMultiplier?: number }, ): number { - options.limitationEnabled = options?.limitationEnabled ?? true; - options.unitMultiplier = options?.unitMultiplier ?? 1; - const parsedInt = parseInt(value); - const numberValue = isNaN(parsedInt) ? Infinity : parsedInt; - return options?.limitationEnabled - ? Math.min(numberValue * options.unitMultiplier, limit) - : numberValue * options.unitMultiplier; + options = { + ...{ + limitationEnabled: false, + unitMultiplier: 1, + fallbackValue: 0, + }, + ...options, + }; + const parsedValue = parseInt(value ?? ''); + if (options?.limitationEnabled) { + return max( + min((!isNaN(parsedValue) ? parsedValue : Infinity) * options.unitMultiplier!, maxLimit), + minLimit, + ); + } + return (!isNaN(parsedValue) ? parsedValue : options.fallbackValue!) * options.unitMultiplier!; + } + + private static isIdTagLocalAuthorized(chargingStation: ChargingStation, idTag: string): boolean { + return ( + chargingStation.hasIdTags() === true && + isNotEmptyString( + chargingStation.idTagsCache + .getIdTags(getIdTagsFile(chargingStation.stationInfo)!) + ?.find((tag) => tag === idTag), + ) + ); + } + + private static async isIdTagRemoteAuthorized( + chargingStation: ChargingStation, + connectorId: number, + idTag: string, + ): Promise { + chargingStation.getConnectorStatus(connectorId)!.authorizeIdTag = idTag; + return ( + ( + await chargingStation.ocppRequestService.requestHandler< + AuthorizeRequest, + AuthorizeResponse + >(chargingStation, RequestCommand.AUTHORIZE, { + idTag, + }) + )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED + ); } private static logPrefix = ( ocppVersion: OCPPVersion, moduleName?: string, - methodName?: string + methodName?: string, ): string => { const logMsg = - Utils.isNotEmptyString(moduleName) && Utils.isNotEmptyString(methodName) + isNotEmptyString(moduleName) && isNotEmptyString(methodName) ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:` : ` OCPP ${ocppVersion} |`; - return Utils.logPrefix(logMsg); + return logPrefix(logMsg); }; }