X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2Focpp%2F1.6%2FOCPP16ServiceUtils.ts;h=7ccd3dcd00b74967b5e6f6ee8d776782df1a405a;hb=8664faa4267611c6f15a5e0d7cd77c6ba72eb7ce;hp=c85ecdd3ee3dba2cac60ce5ae38e79b4fea2d2cd;hpb=5edd8ba0f8978cfb3ca9d80f299d9748c6c5970e;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts index c85ecdd3..7ccd3dcd 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts @@ -1,12 +1,27 @@ // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved. import type { JSONSchemaType } from 'ajv'; +import { + addSeconds, + areIntervalsOverlapping, + differenceInSeconds, + isAfter, + isBefore, + isWithinInterval, +} from 'date-fns'; -import { type ChargingStation, getIdTagsFile } from '../../../charging-station'; +import { OCPP16Constants } from './OCPP16Constants'; +import { + type ChargingStation, + hasFeatureProfile, + hasReservationExpired, +} from '../../../charging-station'; import { OCPPError } from '../../../exception'; import { + type ClearChargingProfileRequest, CurrentType, ErrorType, + type GenericResponse, type JsonType, type MeasurandPerPhaseSampledValueTemplates, type MeasurandValues, @@ -14,9 +29,11 @@ import { MeterValueLocation, MeterValueUnit, OCPP16AuthorizationStatus, - type OCPP16AuthorizeRequest, - type OCPP16AuthorizeResponse, + OCPP16AvailabilityType, + type OCPP16ChangeAvailabilityResponse, + OCPP16ChargePointStatus, type OCPP16ChargingProfile, + type OCPP16ChargingSchedule, type OCPP16IncomingRequestCommand, type OCPP16MeterValue, OCPP16MeterValueMeasurand, @@ -24,6 +41,7 @@ import { OCPP16RequestCommand, type OCPP16SampledValue, OCPP16StandardParametersKey, + OCPP16StopTransactionReason, type OCPP16SupportedFeatureProfiles, OCPPVersion, type SampledValueTemplate, @@ -39,7 +57,6 @@ import { getRandomFloatRounded, getRandomInteger, isNotEmptyArray, - isNotEmptyString, isNullOrUndefined, isUndefined, logger, @@ -53,7 +70,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { featureProfile: OCPP16SupportedFeatureProfiles, command: OCPP16RequestCommand | OCPP16IncomingRequestCommand, ): boolean { - if (!chargingStation.hasFeatureProfile(featureProfile)) { + if (!hasFeatureProfile(chargingStation, featureProfile)) { logger.warn( `${chargingStation.logPrefix()} Trying to '${command}' without '${featureProfile}' feature enabled in ${ OCPP16StandardParametersKey.SupportedFeatureProfiles @@ -147,7 +164,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { OCPP16MeterValueMeasurand.VOLTAGE, phaseLineToNeutralValue as OCPP16MeterValuePhase, ); - let voltagePhaseLineToNeutralMeasurandValue: number; + let voltagePhaseLineToNeutralMeasurandValue: number | undefined; if (voltagePhaseLineToNeutralSampledValueTemplate) { const voltagePhaseLineToNeutralSampledValueTemplateValue = voltagePhaseLineToNeutralSampledValueTemplate.value @@ -182,7 +199,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { OCPP16MeterValueMeasurand.VOLTAGE, phaseLineToLineValue as OCPP16MeterValuePhase, ); - let voltagePhaseLineToLineMeasurandValue: number; + let voltagePhaseLineToLineMeasurandValue: number | undefined; if (voltagePhaseLineToLineSampledValueTemplate) { const voltagePhaseLineToLineSampledValueTemplateValue = voltagePhaseLineToLineSampledValueTemplate.value @@ -243,7 +260,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { if (powerSampledValueTemplate) { OCPP16ServiceUtils.checkMeasurandPowerDivider( chargingStation, - powerSampledValueTemplate.measurand, + powerSampledValueTemplate.measurand!, ); const errMsg = `MeterValues measurand ${ powerSampledValueTemplate.measurand ?? @@ -254,7 +271,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { powerSampledValueTemplate.measurand ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER } measurand value`; - const powerMeasurandValues = {} as MeasurandValues; + const powerMeasurandValues: MeasurandValues = {} as MeasurandValues; const unitDivider = powerSampledValueTemplate?.unit === MeterValueUnit.KILO_WATT ? 1000 : 1; const connectorMaximumAvailablePower = chargingStation.getConnectorMaximumAvailablePower(connectorId); @@ -262,7 +279,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { const connectorMaximumPowerPerPhase = Math.round( connectorMaximumAvailablePower / chargingStation.getNumberOfPhases(), ); - const connectorMinimumPower = Math.round(powerSampledValueTemplate.minimumValue) ?? 0; + const connectorMinimumPower = Math.round(powerSampledValueTemplate.minimumValue!) ?? 0; const connectorMinimumPowerPerPhase = Math.round( connectorMinimumPower / chargingStation.getNumberOfPhases(), ); @@ -281,7 +298,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { Constants.DEFAULT_FLUCTUATION_PERCENT, ); const phase1FluctuatedValue = - powerPerPhaseSampledValueTemplates?.L1?.value && + powerPerPhaseSampledValueTemplates.L1?.value && getRandomFloatFluctuatedRounded( OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( powerPerPhaseSampledValueTemplates.L1.value, @@ -292,7 +309,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { Constants.DEFAULT_FLUCTUATION_PERCENT, ); const phase2FluctuatedValue = - powerPerPhaseSampledValueTemplates?.L2?.value && + powerPerPhaseSampledValueTemplates.L2?.value && getRandomFloatFluctuatedRounded( OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( powerPerPhaseSampledValueTemplates.L2.value, @@ -303,7 +320,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { Constants.DEFAULT_FLUCTUATION_PERCENT, ); const phase3FluctuatedValue = - powerPerPhaseSampledValueTemplates?.L3?.value && + powerPerPhaseSampledValueTemplates.L3?.value && getRandomFloatFluctuatedRounded( OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( powerPerPhaseSampledValueTemplates.L3.value, @@ -314,22 +331,22 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { Constants.DEFAULT_FLUCTUATION_PERCENT, ); powerMeasurandValues.L1 = - phase1FluctuatedValue ?? - defaultFluctuatedPowerPerPhase ?? + (phase1FluctuatedValue as number) ?? + (defaultFluctuatedPowerPerPhase as number) ?? getRandomFloatRounded( connectorMaximumPowerPerPhase / unitDivider, connectorMinimumPowerPerPhase / unitDivider, ); powerMeasurandValues.L2 = - phase2FluctuatedValue ?? - defaultFluctuatedPowerPerPhase ?? + (phase2FluctuatedValue as number) ?? + (defaultFluctuatedPowerPerPhase as number) ?? getRandomFloatRounded( connectorMaximumPowerPerPhase / unitDivider, connectorMinimumPowerPerPhase / unitDivider, ); powerMeasurandValues.L3 = - phase3FluctuatedValue ?? - defaultFluctuatedPowerPerPhase ?? + (phase3FluctuatedValue as number) ?? + (defaultFluctuatedPowerPerPhase as number) ?? getRandomFloatRounded( connectorMaximumPowerPerPhase / unitDivider, connectorMinimumPowerPerPhase / unitDivider, @@ -410,9 +427,10 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { const phaseValue = `L${phase}-N`; meterValue.sampledValue.push( OCPP16ServiceUtils.buildSampledValue( - (powerPerPhaseSampledValueTemplates[`L${phase}`] as SampledValueTemplate) ?? - powerSampledValueTemplate, - powerMeasurandValues[`L${phase}`] as number, + powerPerPhaseSampledValueTemplates[ + `L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates + ]! ?? powerSampledValueTemplate, + powerMeasurandValues[`L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates], undefined, phaseValue as OCPP16MeterValuePhase, ), @@ -478,7 +496,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { if (currentSampledValueTemplate) { OCPP16ServiceUtils.checkMeasurandPowerDivider( chargingStation, - currentSampledValueTemplate.measurand, + currentSampledValueTemplate.measurand!, ); const errMsg = `MeterValues measurand ${ currentSampledValueTemplate.measurand ?? @@ -514,7 +532,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { Constants.DEFAULT_FLUCTUATION_PERCENT, ); const phase1FluctuatedValue = - currentPerPhaseSampledValueTemplates?.L1?.value && + currentPerPhaseSampledValueTemplates.L1?.value && getRandomFloatFluctuatedRounded( OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( currentPerPhaseSampledValueTemplates.L1.value, @@ -525,7 +543,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { Constants.DEFAULT_FLUCTUATION_PERCENT, ); const phase2FluctuatedValue = - currentPerPhaseSampledValueTemplates?.L2?.value && + currentPerPhaseSampledValueTemplates.L2?.value && getRandomFloatFluctuatedRounded( OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( currentPerPhaseSampledValueTemplates.L2.value, @@ -536,7 +554,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { Constants.DEFAULT_FLUCTUATION_PERCENT, ); const phase3FluctuatedValue = - currentPerPhaseSampledValueTemplates?.L3?.value && + currentPerPhaseSampledValueTemplates.L3?.value && getRandomFloatFluctuatedRounded( OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( currentPerPhaseSampledValueTemplates.L3.value, @@ -547,16 +565,16 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { Constants.DEFAULT_FLUCTUATION_PERCENT, ); currentMeasurandValues.L1 = - phase1FluctuatedValue ?? - defaultFluctuatedAmperagePerPhase ?? + (phase1FluctuatedValue as number) ?? + (defaultFluctuatedAmperagePerPhase as number) ?? getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage); currentMeasurandValues.L2 = - phase2FluctuatedValue ?? - defaultFluctuatedAmperagePerPhase ?? + (phase2FluctuatedValue as number) ?? + (defaultFluctuatedAmperagePerPhase as number) ?? getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage); currentMeasurandValues.L3 = - phase3FluctuatedValue ?? - defaultFluctuatedAmperagePerPhase ?? + (phase3FluctuatedValue as number) ?? + (defaultFluctuatedAmperagePerPhase as number) ?? getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage); } else { currentMeasurandValues.L1 = currentSampledValueTemplate.value @@ -631,9 +649,10 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { const phaseValue = `L${phase}`; meterValue.sampledValue.push( OCPP16ServiceUtils.buildSampledValue( - (currentPerPhaseSampledValueTemplates[phaseValue] as SampledValueTemplate) ?? - currentSampledValueTemplate, - currentMeasurandValues[phaseValue] as number, + currentPerPhaseSampledValueTemplates[ + phaseValue as keyof MeasurandPerPhaseSampledValueTemplates + ]! ?? currentSampledValueTemplate, + currentMeasurandValues[phaseValue as keyof MeasurandPerPhaseSampledValueTemplates], undefined, phaseValue as OCPP16MeterValuePhase, ), @@ -667,7 +686,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { if (energySampledValueTemplate) { OCPP16ServiceUtils.checkMeasurandPowerDivider( chargingStation, - energySampledValueTemplate.measurand, + energySampledValueTemplate.measurand!, ); const unitDivider = energySampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1; @@ -692,18 +711,19 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { ) : getRandomFloatRounded(connectorMaximumEnergyRounded); // Persist previous value on connector - if ( - connector && - isNullOrUndefined(connector.energyActiveImportRegisterValue) === false && - connector.energyActiveImportRegisterValue >= 0 && - isNullOrUndefined(connector.transactionEnergyActiveImportRegisterValue) === false && - connector.transactionEnergyActiveImportRegisterValue >= 0 - ) { - connector.energyActiveImportRegisterValue += energyValueRounded; - connector.transactionEnergyActiveImportRegisterValue += energyValueRounded; - } else { - connector.energyActiveImportRegisterValue = 0; - connector.transactionEnergyActiveImportRegisterValue = 0; + if (connector) { + if ( + isNullOrUndefined(connector.energyActiveImportRegisterValue) === false && + connector.energyActiveImportRegisterValue! >= 0 && + isNullOrUndefined(connector.transactionEnergyActiveImportRegisterValue) === false && + connector.transactionEnergyActiveImportRegisterValue! >= 0 + ) { + connector.energyActiveImportRegisterValue! += energyValueRounded; + connector.transactionEnergyActiveImportRegisterValue! += energyValueRounded; + } else { + connector.energyActiveImportRegisterValue = 0; + connector.transactionEnergyActiveImportRegisterValue = 0; + } } meterValue.sampledValue.push( OCPP16ServiceUtils.buildSampledValue( @@ -721,10 +741,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { `${chargingStation.logPrefix()} MeterValues measurand ${ meterValue.sampledValue[sampledValuesIndex].measurand ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER - }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${roundTo( - interval / (3600 * 1000), - 4, - )}h`, + }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${interval}ms`, ); } } @@ -748,7 +765,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1; meterValue.sampledValue.push( OCPP16ServiceUtils.buildSampledValue( - sampledValueTemplate, + sampledValueTemplate!, roundTo((meterStart ?? 0) / unitDivider, 4), MeterValueContext.TRANSACTION_BEGIN, ), @@ -773,7 +790,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1; meterValue.sampledValue.push( OCPP16ServiceUtils.buildSampledValue( - sampledValueTemplate, + sampledValueTemplate!, roundTo((meterStop ?? 0) / unitDivider, 4), MeterValueContext.TRANSACTION_END, ), @@ -791,6 +808,55 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { return meterValues; } + public static remoteStopTransaction = async ( + chargingStation: ChargingStation, + connectorId: number, + ): Promise => { + await OCPP16ServiceUtils.sendAndSetConnectorStatus( + chargingStation, + connectorId, + OCPP16ChargePointStatus.Finishing, + ); + const stopResponse = await chargingStation.stopTransactionOnConnector( + connectorId, + OCPP16StopTransactionReason.REMOTE, + ); + if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) { + return OCPP16Constants.OCPP_RESPONSE_ACCEPTED; + } + return OCPP16Constants.OCPP_RESPONSE_REJECTED; + }; + + public static changeAvailability = async ( + chargingStation: ChargingStation, + connectorIds: number[], + chargePointStatus: OCPP16ChargePointStatus, + availabilityType: OCPP16AvailabilityType, + ): Promise => { + const responses: OCPP16ChangeAvailabilityResponse[] = []; + for (const connectorId of connectorIds) { + let response: OCPP16ChangeAvailabilityResponse = + OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED; + const connectorStatus = chargingStation.getConnectorStatus(connectorId)!; + if (connectorStatus?.transactionStarted === true) { + response = OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED; + } + connectorStatus.availability = availabilityType; + if (response === OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED) { + await OCPP16ServiceUtils.sendAndSetConnectorStatus( + chargingStation, + connectorId, + chargePointStatus, + ); + } + responses.push(response); + } + if (responses.includes(OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED)) { + return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED; + } + return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED; + }; + public static setChargingProfile( chargingStation: ChargingStation, connectorId: number, @@ -800,15 +866,15 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { logger.error( `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an uninitialized charging profiles array attribute, applying deferred initialization`, ); - chargingStation.getConnectorStatus(connectorId).chargingProfiles = []; + chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = []; } if ( Array.isArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles) === false ) { logger.error( - `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an improper attribute type for the charging profiles array, applying proper type initialization`, + `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an improper attribute type for the charging profiles array, applying proper type deferred initialization`, ); - chargingStation.getConnectorStatus(connectorId).chargingProfiles = []; + chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = []; } let cpReplaced = false; if (isNotEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) { @@ -820,7 +886,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { (chargingProfile.stackLevel === cp.stackLevel && chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose) ) { - chargingStation.getConnectorStatus(connectorId).chargingProfiles[index] = cp; + chargingStation.getConnectorStatus(connectorId)!.chargingProfiles![index] = cp; cpReplaced = true; } }); @@ -828,6 +894,264 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { !cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp); } + public static clearChargingProfiles = ( + chargingStation: ChargingStation, + commandPayload: ClearChargingProfileRequest, + chargingProfiles: OCPP16ChargingProfile[] | undefined, + ): boolean => { + const { id, chargingProfilePurpose, stackLevel } = commandPayload; + let clearedCP = false; + if (isNotEmptyArray(chargingProfiles)) { + chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => { + let clearCurrentCP = false; + if (chargingProfile.chargingProfileId === id) { + clearCurrentCP = true; + } + if (!chargingProfilePurpose && chargingProfile.stackLevel === stackLevel) { + clearCurrentCP = true; + } + if (!stackLevel && chargingProfile.chargingProfilePurpose === chargingProfilePurpose) { + clearCurrentCP = true; + } + if ( + chargingProfile.stackLevel === stackLevel && + chargingProfile.chargingProfilePurpose === chargingProfilePurpose + ) { + clearCurrentCP = true; + } + if (clearCurrentCP) { + chargingProfiles.splice(index, 1); + logger.debug( + `${chargingStation.logPrefix()} Matching charging profile(s) cleared: %j`, + chargingProfile, + ); + clearedCP = true; + } + }); + } + return clearedCP; + }; + + public static composeChargingSchedules = ( + chargingScheduleHigher: OCPP16ChargingSchedule | undefined, + chargingScheduleLower: OCPP16ChargingSchedule | undefined, + compositeInterval: Interval, + ): OCPP16ChargingSchedule | undefined => { + if (!chargingScheduleHigher && !chargingScheduleLower) { + return undefined; + } + if (chargingScheduleHigher && !chargingScheduleLower) { + return OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleHigher, compositeInterval); + } + if (!chargingScheduleHigher && chargingScheduleLower) { + return OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleLower, compositeInterval); + } + const compositeChargingScheduleHigher: OCPP16ChargingSchedule | undefined = + OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleHigher!, compositeInterval); + const compositeChargingScheduleLower: OCPP16ChargingSchedule | undefined = + OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleLower!, compositeInterval); + const compositeChargingScheduleHigherInterval: Interval = { + start: compositeChargingScheduleHigher!.startSchedule!, + end: addSeconds( + compositeChargingScheduleHigher!.startSchedule!, + compositeChargingScheduleHigher!.duration!, + ), + }; + const compositeChargingScheduleLowerInterval: Interval = { + start: compositeChargingScheduleLower!.startSchedule!, + end: addSeconds( + compositeChargingScheduleLower!.startSchedule!, + compositeChargingScheduleLower!.duration!, + ), + }; + const higherFirst = isBefore( + compositeChargingScheduleHigherInterval.start, + compositeChargingScheduleLowerInterval.start, + ); + if ( + !areIntervalsOverlapping( + compositeChargingScheduleHigherInterval, + compositeChargingScheduleLowerInterval, + ) + ) { + return { + ...compositeChargingScheduleLower, + ...compositeChargingScheduleHigher!, + startSchedule: higherFirst + ? (compositeChargingScheduleHigherInterval.start as Date) + : (compositeChargingScheduleLowerInterval.start as Date), + duration: higherFirst + ? differenceInSeconds( + compositeChargingScheduleLowerInterval.end, + compositeChargingScheduleHigherInterval.start, + ) + : differenceInSeconds( + compositeChargingScheduleHigherInterval.end, + compositeChargingScheduleLowerInterval.start, + ), + chargingSchedulePeriod: [ + ...compositeChargingScheduleHigher!.chargingSchedulePeriod.map((schedulePeriod) => { + return { + ...schedulePeriod, + startPeriod: higherFirst + ? 0 + : schedulePeriod.startPeriod + + differenceInSeconds( + compositeChargingScheduleHigherInterval.start, + compositeChargingScheduleLowerInterval.start, + ), + }; + }), + ...compositeChargingScheduleLower!.chargingSchedulePeriod.map((schedulePeriod) => { + return { + ...schedulePeriod, + startPeriod: higherFirst + ? schedulePeriod.startPeriod + + differenceInSeconds( + compositeChargingScheduleLowerInterval.start, + compositeChargingScheduleHigherInterval.start, + ) + : 0, + }; + }), + ].sort((a, b) => a.startPeriod - b.startPeriod), + }; + } + return { + ...compositeChargingScheduleLower, + ...compositeChargingScheduleHigher!, + startSchedule: higherFirst + ? (compositeChargingScheduleHigherInterval.start as Date) + : (compositeChargingScheduleLowerInterval.start as Date), + duration: higherFirst + ? differenceInSeconds( + compositeChargingScheduleLowerInterval.end, + compositeChargingScheduleHigherInterval.start, + ) + : differenceInSeconds( + compositeChargingScheduleHigherInterval.end, + compositeChargingScheduleLowerInterval.start, + ), + chargingSchedulePeriod: [ + ...compositeChargingScheduleHigher!.chargingSchedulePeriod.map((schedulePeriod) => { + return { + ...schedulePeriod, + startPeriod: higherFirst + ? 0 + : schedulePeriod.startPeriod + + differenceInSeconds( + compositeChargingScheduleHigherInterval.start, + compositeChargingScheduleLowerInterval.start, + ), + }; + }), + ...compositeChargingScheduleLower!.chargingSchedulePeriod + .filter((schedulePeriod, index) => { + if ( + higherFirst && + isWithinInterval( + addSeconds( + compositeChargingScheduleLowerInterval.start, + schedulePeriod.startPeriod, + ), + { + start: compositeChargingScheduleLowerInterval.start, + end: compositeChargingScheduleHigherInterval.end, + }, + ) + ) { + return false; + } + if ( + higherFirst && + index < compositeChargingScheduleLower!.chargingSchedulePeriod.length - 1 && + !isWithinInterval( + addSeconds( + compositeChargingScheduleLowerInterval.start, + schedulePeriod.startPeriod, + ), + { + start: compositeChargingScheduleLowerInterval.start, + end: compositeChargingScheduleHigherInterval.end, + }, + ) && + isWithinInterval( + addSeconds( + compositeChargingScheduleLowerInterval.start, + compositeChargingScheduleLower!.chargingSchedulePeriod[index + 1].startPeriod, + ), + { + start: compositeChargingScheduleLowerInterval.start, + end: compositeChargingScheduleHigherInterval.end, + }, + ) + ) { + return false; + } + if ( + !higherFirst && + isWithinInterval( + addSeconds( + compositeChargingScheduleLowerInterval.start, + schedulePeriod.startPeriod, + ), + { + start: compositeChargingScheduleHigherInterval.start, + end: compositeChargingScheduleLowerInterval.end, + }, + ) + ) { + return false; + } + return true; + }) + .map((schedulePeriod, index) => { + if (index === 0 && schedulePeriod.startPeriod !== 0) { + schedulePeriod.startPeriod = 0; + } + return { + ...schedulePeriod, + startPeriod: higherFirst + ? schedulePeriod.startPeriod + + differenceInSeconds( + compositeChargingScheduleLowerInterval.start, + compositeChargingScheduleHigherInterval.start, + ) + : 0, + }; + }), + ].sort((a, b) => a.startPeriod - b.startPeriod), + }; + }; + + public static hasReservation = ( + chargingStation: ChargingStation, + connectorId: number, + idTag: string, + ): boolean => { + const connectorReservation = chargingStation.getReservationBy('connectorId', connectorId); + const chargingStationReservation = chargingStation.getReservationBy('connectorId', 0); + if ( + (chargingStation.getConnectorStatus(connectorId)?.status === + OCPP16ChargePointStatus.Reserved && + connectorReservation && + !hasReservationExpired(connectorReservation) && + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + connectorReservation?.idTag === idTag) || + (chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved && + chargingStationReservation && + !hasReservationExpired(chargingStationReservation) && + chargingStationReservation?.idTag === idTag) + ) { + logger.debug( + `${chargingStation.logPrefix()} Connector id ${connectorId} has a valid reservation for idTag ${idTag}: %j`, + connectorReservation ?? chargingStationReservation, + ); + return true; + } + return false; + }; + public static parseJsonSchemaFile( relativePath: string, moduleName?: string, @@ -841,28 +1165,78 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { ); } - public static async isIdTagAuthorized( - chargingStation: ChargingStation, - connectorId: number, - idTag: string, - ): Promise { - let authorized = false; - const connectorStatus = chargingStation.getConnectorStatus(connectorId); - if (OCPP16ServiceUtils.isIdTagLocalAuthorized(chargingStation, idTag)) { - connectorStatus.localAuthorizeIdTag = idTag; - connectorStatus.idTagLocalAuthorized = true; - authorized = true; - } else if (chargingStation.getMustAuthorizeAtRemoteStart() === true) { - connectorStatus.authorizeIdTag = idTag; - authorized = await OCPP16ServiceUtils.isIdTagRemoteAuthorized(chargingStation, idTag); - } else { - logger.warn( - `${chargingStation.logPrefix()} The charging station configuration expects authorize at - remote start transaction but local authorization or authorize isn't enabled`, - ); + private static composeChargingSchedule = ( + chargingSchedule: OCPP16ChargingSchedule, + compositeInterval: Interval, + ): OCPP16ChargingSchedule | undefined => { + const chargingScheduleInterval: Interval = { + start: chargingSchedule.startSchedule!, + end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!), + }; + if (areIntervalsOverlapping(chargingScheduleInterval, compositeInterval)) { + chargingSchedule.chargingSchedulePeriod.sort((a, b) => a.startPeriod - b.startPeriod); + if (isBefore(chargingScheduleInterval.start, compositeInterval.start)) { + return { + ...chargingSchedule, + startSchedule: compositeInterval.start as Date, + duration: differenceInSeconds( + chargingScheduleInterval.end, + compositeInterval.start as Date, + ), + chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod + .filter((schedulePeriod, index) => { + if ( + isWithinInterval( + addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!, + compositeInterval, + ) + ) { + return true; + } + if ( + index < chargingSchedule.chargingSchedulePeriod.length - 1 && + !isWithinInterval( + addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod), + compositeInterval, + ) && + isWithinInterval( + addSeconds( + chargingScheduleInterval.start, + chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod, + ), + compositeInterval, + ) + ) { + return true; + } + return false; + }) + .map((schedulePeriod, index) => { + if (index === 0 && schedulePeriod.startPeriod !== 0) { + schedulePeriod.startPeriod = 0; + } + return schedulePeriod; + }), + }; + } + if (isAfter(chargingScheduleInterval.end, compositeInterval.end)) { + return { + ...chargingSchedule, + duration: differenceInSeconds( + compositeInterval.end as Date, + chargingScheduleInterval.start, + ), + chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter((schedulePeriod) => + isWithinInterval( + addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!, + compositeInterval, + ), + ), + }; + } + return chargingSchedule; } - return authorized; - } + }; private static buildSampledValue( sampledValueTemplate: SampledValueTemplate, @@ -874,7 +1248,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { const sampledValueContext = context ?? sampledValueTemplate?.context ?? null; const sampledValueLocation = sampledValueTemplate?.location ?? - OCPP16ServiceUtils.getMeasurandDefaultLocation(sampledValueTemplate?.measurand ?? null); + OCPP16ServiceUtils.getMeasurandDefaultLocation(sampledValueTemplate.measurand!); const sampledValuePhase = phase ?? sampledValueTemplate?.phase ?? null; return { ...(!isNullOrUndefined(sampledValueTemplate.unit) && { @@ -887,7 +1261,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { ...(!isNullOrUndefined(sampledValueLocation) && { location: sampledValueLocation }), ...(!isNullOrUndefined(sampledValueValue) && { value: sampledValueValue.toString() }), ...(!isNullOrUndefined(sampledValuePhase) && { phase: sampledValuePhase }), - }; + } as OCPP16SampledValue; } private static checkMeasurandPowerDivider( @@ -918,51 +1292,25 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { } } - private static getMeasurandDefaultUnit( - measurandType: OCPP16MeterValueMeasurand, - ): MeterValueUnit | undefined { - switch (measurandType) { - case OCPP16MeterValueMeasurand.CURRENT_EXPORT: - case OCPP16MeterValueMeasurand.CURRENT_IMPORT: - case OCPP16MeterValueMeasurand.CURRENT_OFFERED: - return MeterValueUnit.AMP; - case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER: - case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER: - return MeterValueUnit.WATT_HOUR; - case OCPP16MeterValueMeasurand.POWER_ACTIVE_EXPORT: - case OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT: - case OCPP16MeterValueMeasurand.POWER_OFFERED: - return MeterValueUnit.WATT; - case OCPP16MeterValueMeasurand.STATE_OF_CHARGE: - return MeterValueUnit.PERCENT; - case OCPP16MeterValueMeasurand.VOLTAGE: - return MeterValueUnit.VOLT; - } - } - - private static isIdTagLocalAuthorized(chargingStation: ChargingStation, idTag: string): boolean { - return ( - chargingStation.getLocalAuthListEnabled() === true && - chargingStation.hasIdTags() === true && - isNotEmptyString( - chargingStation.idTagsCache - .getIdTags(getIdTagsFile(chargingStation.stationInfo)) - ?.find((tag) => tag === idTag), - ) - ); - } - - private static async isIdTagRemoteAuthorized( - chargingStation: ChargingStation, - idTag: string, - ): Promise { - const authorizeResponse: OCPP16AuthorizeResponse = - await chargingStation.ocppRequestService.requestHandler< - OCPP16AuthorizeRequest, - OCPP16AuthorizeResponse - >(chargingStation, OCPP16RequestCommand.AUTHORIZE, { - idTag: idTag, - }); - return authorizeResponse?.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED; - } + // private static getMeasurandDefaultUnit( + // measurandType: OCPP16MeterValueMeasurand, + // ): MeterValueUnit | undefined { + // switch (measurandType) { + // case OCPP16MeterValueMeasurand.CURRENT_EXPORT: + // case OCPP16MeterValueMeasurand.CURRENT_IMPORT: + // case OCPP16MeterValueMeasurand.CURRENT_OFFERED: + // return MeterValueUnit.AMP; + // case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER: + // case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER: + // return MeterValueUnit.WATT_HOUR; + // case OCPP16MeterValueMeasurand.POWER_ACTIVE_EXPORT: + // case OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT: + // case OCPP16MeterValueMeasurand.POWER_OFFERED: + // return MeterValueUnit.WATT; + // case OCPP16MeterValueMeasurand.STATE_OF_CHARGE: + // return MeterValueUnit.PERCENT; + // case OCPP16MeterValueMeasurand.VOLTAGE: + // return MeterValueUnit.VOLT; + // } + // } }