import type { JSONSchemaType } from 'ajv';
import { Client, type FTPResponse } from 'basic-ftp';
-import {
- addSeconds,
- differenceInSeconds,
- isAfter,
- isBefore,
- isDate,
- isWithinInterval,
- max,
- maxTime,
- min,
- minTime,
- secondsToMilliseconds,
-} from 'date-fns';
+import { addSeconds, differenceInSeconds, isDate, maxTime, secondsToMilliseconds } from 'date-fns';
import { create } from 'tar';
import { OCPP16Constants } from './OCPP16Constants';
OCPP16ChargePointStatus,
type OCPP16ChargingProfile,
OCPP16ChargingProfilePurposeType,
- OCPP16ChargingRateUnitType,
type OCPP16ChargingSchedule,
- type OCPP16ChargingSchedulePeriod,
type OCPP16ClearCacheRequest,
type OCPP16DataTransferRequest,
type OCPP16DataTransferResponse,
isNotEmptyString,
isNullOrUndefined,
isUndefined,
- isValidTime,
logger,
sleep,
} from '../../../utils';
if (chargingStation.hasConnector(connectorId) === false) {
logger.error(
`${chargingStation.logPrefix()} Trying to unlock a non existing
- connector id ${connectorId.toString()}`,
+ connector id ${connectorId}`,
);
return OCPP16Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED;
}
if (connectorId === 0) {
- logger.error(
- `${chargingStation.logPrefix()} Trying to unlock connector id ${connectorId.toString()}`,
- );
+ logger.error(`${chargingStation.logPrefix()} Trying to unlock connector id ${connectorId}`);
return OCPP16Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED;
}
if (chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
}
if (
csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE &&
- (connectorId === 0 ||
- chargingStation.getConnectorStatus(connectorId)?.transactionStarted === false)
+ connectorId === 0
+ ) {
+ logger.error(
+ `${chargingStation.logPrefix()} Trying to set transaction charging profile(s)
+ on connector ${connectorId}`,
+ );
+ return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED;
+ }
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId);
+ if (
+ csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE &&
+ connectorId > 0 &&
+ connectorStatus?.transactionStarted === false
) {
logger.error(
`${chargingStation.logPrefix()} Trying to set transaction charging profile(s)
);
return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED;
}
+ if (
+ csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE &&
+ connectorId > 0 &&
+ connectorStatus?.transactionStarted === true &&
+ csChargingProfiles.transactionId !== connectorStatus?.transactionId
+ ) {
+ logger.error(
+ `${chargingStation.logPrefix()} Trying to set transaction charging profile(s)
+ on connector ${connectorId} with a different transaction id ${
+ csChargingProfiles.transactionId
+ } than the started transaction id ${connectorStatus?.transactionId}`,
+ );
+ return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED;
+ }
OCPP16ServiceUtils.setChargingProfile(chargingStation, connectorId, csChargingProfiles);
logger.debug(
`${chargingStation.logPrefix()} Charging profile(s) set on connector id ${connectorId}: %j`,
return OCPP16Constants.OCPP_RESPONSE_REJECTED;
}
const currentDate = new Date();
- const interval: Interval = {
+ const compositeScheduleInterval: Interval = {
start: currentDate,
end: addSeconds(currentDate, duration),
};
// Get charging profiles sorted by connector id then stack level
- const storedChargingProfiles: OCPP16ChargingProfile[] = getConnectorChargingProfiles(
+ const chargingProfiles: OCPP16ChargingProfile[] = getConnectorChargingProfiles(
chargingStation,
connectorId,
);
- const chargingProfiles: OCPP16ChargingProfile[] = [];
- for (const storedChargingProfile of storedChargingProfiles) {
+ let previousCompositeSchedule: OCPP16ChargingSchedule | undefined;
+ let compositeSchedule: OCPP16ChargingSchedule | undefined;
+ for (const chargingProfile of chargingProfiles) {
if (
- connectorStatus?.transactionStarted &&
- isNullOrUndefined(storedChargingProfile.chargingSchedule?.startSchedule)
+ isNullOrUndefined(chargingProfile.chargingSchedule?.startSchedule) &&
+ connectorStatus?.transactionStarted
) {
logger.debug(
`${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${
- storedChargingProfile.chargingProfileId
+ chargingProfile.chargingProfileId
} has no startSchedule defined. Trying to set it to the connector current transaction start date`,
);
// OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
- storedChargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart;
+ chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart;
}
- if (!isDate(storedChargingProfile.chargingSchedule?.startSchedule)) {
+ if (
+ !isNullOrUndefined(chargingProfile.chargingSchedule?.startSchedule) &&
+ !isDate(chargingProfile.chargingSchedule?.startSchedule)
+ ) {
logger.warn(
`${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${
- storedChargingProfile.chargingProfileId
- } startSchedule property is not a Date object. Trying to convert it to a Date object`,
+ chargingProfile.chargingProfileId
+ } startSchedule property is not a Date instance. Trying to convert it to a Date instance`,
);
- storedChargingProfile.chargingSchedule.startSchedule = convertToDate(
- storedChargingProfile.chargingSchedule?.startSchedule,
+ chargingProfile.chargingSchedule.startSchedule = convertToDate(
+ chargingProfile.chargingSchedule?.startSchedule,
)!;
}
+ if (
+ !isNullOrUndefined(chargingProfile.chargingSchedule?.startSchedule) &&
+ isNullOrUndefined(chargingProfile.chargingSchedule?.duration)
+ ) {
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${
+ chargingProfile.chargingProfileId
+ } has no duration defined and will be set to the maximum time allowed`,
+ );
+ // OCPP specifies that if duration is not defined, it should be infinite
+ chargingProfile.chargingSchedule.duration = differenceInSeconds(
+ maxTime,
+ chargingProfile.chargingSchedule.startSchedule!,
+ );
+ }
if (
!prepareChargingProfileKind(
connectorStatus,
- storedChargingProfile,
- interval.start as Date,
+ chargingProfile,
+ compositeScheduleInterval.start as Date,
chargingStation.logPrefix(),
)
) {
}
if (
!canProceedChargingProfile(
- storedChargingProfile,
- interval.start as Date,
+ chargingProfile,
+ compositeScheduleInterval.start as Date,
chargingStation.logPrefix(),
)
) {
continue;
}
- // Add active charging profiles into chargingProfiles array
- if (
- isValidTime(storedChargingProfile.chargingSchedule?.startSchedule) &&
- isWithinInterval(storedChargingProfile.chargingSchedule.startSchedule!, interval)
- ) {
- if (isEmptyArray(chargingProfiles)) {
- if (
- isAfter(
- addSeconds(
- storedChargingProfile.chargingSchedule.startSchedule!,
- storedChargingProfile.chargingSchedule.duration!,
- ),
- interval.end,
- )
- ) {
- storedChargingProfile.chargingSchedule.chargingSchedulePeriod =
- storedChargingProfile.chargingSchedule.chargingSchedulePeriod.filter(
- (schedulePeriod) =>
- isWithinInterval(
- addSeconds(
- storedChargingProfile.chargingSchedule.startSchedule!,
- schedulePeriod.startPeriod,
- )!,
- interval,
- ),
- );
- storedChargingProfile.chargingSchedule.duration = differenceInSeconds(
- interval.end,
- storedChargingProfile.chargingSchedule.startSchedule!,
- );
- }
- chargingProfiles.push(storedChargingProfile);
- } else if (isNotEmptyArray(chargingProfiles)) {
- const chargingProfilesInterval: Interval = {
- start: min(
- chargingProfiles.map(
- (chargingProfile) => chargingProfile.chargingSchedule.startSchedule ?? maxTime,
- ),
- ),
- end: max(
- chargingProfiles.map(
- (chargingProfile) =>
- addSeconds(
- chargingProfile.chargingSchedule.startSchedule!,
- chargingProfile.chargingSchedule.duration!,
- ) ?? minTime,
- ),
- ),
- };
- let addChargingProfile = false;
- if (
- isBefore(interval.start, chargingProfilesInterval.start) &&
- isBefore(
- storedChargingProfile.chargingSchedule.startSchedule!,
- chargingProfilesInterval.start,
- )
- ) {
- // Remove charging schedule periods that are after the start of the active profiles interval
- storedChargingProfile.chargingSchedule.chargingSchedulePeriod =
- storedChargingProfile.chargingSchedule.chargingSchedulePeriod.filter(
- (schedulePeriod) =>
- isWithinInterval(
- addSeconds(
- storedChargingProfile.chargingSchedule.startSchedule!,
- schedulePeriod.startPeriod,
- ),
- {
- start: interval.start,
- end: chargingProfilesInterval.start,
- },
- ),
- );
- addChargingProfile = true;
- }
- if (
- isBefore(chargingProfilesInterval.end, interval.end) &&
- isAfter(
- addSeconds(
- storedChargingProfile.chargingSchedule.startSchedule!,
- storedChargingProfile.chargingSchedule.duration!,
- ),
- chargingProfilesInterval.end,
- )
- ) {
- // Remove charging schedule periods that are before the end of the active profiles interval
- // FIXME: can lead to a gap in the charging schedule: chargingProfilesInterval.end -> first matching schedulePeriod.startPeriod
- storedChargingProfile.chargingSchedule.chargingSchedulePeriod =
- storedChargingProfile.chargingSchedule.chargingSchedulePeriod.filter(
- (schedulePeriod) =>
- isWithinInterval(
- addSeconds(
- storedChargingProfile.chargingSchedule.startSchedule!,
- schedulePeriod.startPeriod,
- ),
- {
- start: chargingProfilesInterval.end,
- end: interval.end,
- },
- ),
- );
- addChargingProfile = true;
- }
- addChargingProfile && chargingProfiles.push(storedChargingProfile);
- }
- }
- }
- const compositeScheduleStart: Date = min(
- chargingProfiles.map(
- (chargingProfile) => chargingProfile.chargingSchedule.startSchedule ?? maxTime,
- ),
- );
- const compositeScheduleDuration: number = Math.max(
- ...chargingProfiles.map((chargingProfile) =>
- isNaN(chargingProfile.chargingSchedule.duration!)
- ? -Infinity
- : chargingProfile.chargingSchedule.duration!,
- ),
- );
- const compositeSchedulePeriods: OCPP16ChargingSchedulePeriod[] = chargingProfiles
- .map((chargingProfile) => chargingProfile.chargingSchedule.chargingSchedulePeriod)
- .reduce(
- (accumulator, value) =>
- accumulator.concat(value).sort((a, b) => a.startPeriod - b.startPeriod),
- [],
+ compositeSchedule = OCPP16ServiceUtils.composeChargingSchedules(
+ previousCompositeSchedule,
+ chargingProfile.chargingSchedule,
+ compositeScheduleInterval,
);
- const compositeSchedule: OCPP16ChargingSchedule = {
- startSchedule: compositeScheduleStart,
- duration: compositeScheduleDuration,
- chargingRateUnit: chargingProfiles.every(
- (chargingProfile) =>
- chargingProfile.chargingSchedule.chargingRateUnit === OCPP16ChargingRateUnitType.AMPERE,
- )
- ? OCPP16ChargingRateUnitType.AMPERE
- : chargingProfiles.every(
- (chargingProfile) =>
- chargingProfile.chargingSchedule.chargingRateUnit === OCPP16ChargingRateUnitType.WATT,
- )
- ? OCPP16ChargingRateUnitType.WATT
- : OCPP16ChargingRateUnitType.AMPERE,
- chargingSchedulePeriod: compositeSchedulePeriods,
- minChargeRate: Math.min(
- ...chargingProfiles.map((chargingProfile) =>
- isNaN(chargingProfile.chargingSchedule.minChargeRate!)
- ? Infinity
- : chargingProfile.chargingSchedule.minChargeRate!,
- ),
- ),
- };
- return {
- status: GenericStatus.Accepted,
- scheduleStart: compositeSchedule.startSchedule!,
- connectorId,
- chargingSchedule: compositeSchedule,
- };
+ previousCompositeSchedule = compositeSchedule;
+ }
+ if (compositeSchedule) {
+ return {
+ status: GenericStatus.Accepted,
+ scheduleStart: compositeSchedule.startSchedule!,
+ connectorId,
+ chargingSchedule: compositeSchedule,
+ };
+ }
+ return OCPP16Constants.OCPP_RESPONSE_REJECTED;
}
private handleRequestClearChargingProfile(
);
return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN;
}
- if (
- !isNullOrUndefined(connectorId) &&
- isNotEmptyArray(chargingStation.getConnectorStatus(connectorId!)?.chargingProfiles)
- ) {
- chargingStation.getConnectorStatus(connectorId!)!.chargingProfiles = [];
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId!);
+ if (!isNullOrUndefined(connectorId) && isNotEmptyArray(connectorStatus?.chargingProfiles)) {
+ connectorStatus!.chargingProfiles = [];
logger.debug(
`${chargingStation.logPrefix()} Charging profile(s) cleared on connector id ${connectorId}`,
);
let clearedCP = false;
if (chargingStation.hasEvses) {
for (const evseStatus of chargingStation.evses.values()) {
- for (const connectorStatus of evseStatus.connectors.values()) {
+ for (const status of evseStatus.connectors.values()) {
clearedCP = OCPP16ServiceUtils.clearChargingProfiles(
chargingStation,
commandPayload,
- connectorStatus.chargingProfiles,
+ status.chargingProfiles,
);
}
}
if (chargingStation.hasConnector(connectorId) === false) {
logger.error(
`${chargingStation.logPrefix()} Trying to change the availability of a
- non existing connector id ${connectorId.toString()}`,
+ non existing connector id ${connectorId}`,
);
return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED;
}
const remoteStartTransactionLogMsg = `
${chargingStation.logPrefix()} Transaction remotely STARTED on ${
chargingStation.stationInfo.chargingStationId
- }#${transactionConnectorId.toString()} for idTag '${idTag}'`;
+ }#${transactionConnectorId} for idTag '${idTag}'`;
await OCPP16ServiceUtils.sendAndSetConnectorStatus(
chargingStation,
transactionConnectorId,
) {
// Authorization successful, start transaction
if (
- this.setRemoteStartTransactionChargingProfile(
- chargingStation,
- transactionConnectorId,
- chargingProfile!,
- ) === true
+ (chargingProfile &&
+ this.setRemoteStartTransactionChargingProfile(
+ chargingStation,
+ transactionConnectorId,
+ chargingProfile,
+ ) === true) ??
+ !chargingProfile
) {
connectorStatus.transactionRemoteStarted = true;
if (
}
// No authorization check required, start transaction
if (
- this.setRemoteStartTransactionChargingProfile(
- chargingStation,
- transactionConnectorId,
- chargingProfile!,
- ) === true
+ (chargingProfile &&
+ this.setRemoteStartTransactionChargingProfile(
+ chargingStation,
+ transactionConnectorId,
+ chargingProfile,
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ ) === true) ||
+ !chargingProfile
) {
connectorStatus.transactionRemoteStarted = true;
if (
connectorId: number,
idTag: string,
): Promise<GenericResponse> {
- if (
- chargingStation.getConnectorStatus(connectorId)?.status !== OCPP16ChargePointStatus.Available
- ) {
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId);
+ if (connectorStatus?.status !== OCPP16ChargePointStatus.Available) {
await OCPP16ServiceUtils.sendAndSetConnectorStatus(
chargingStation,
connectorId,
}
logger.warn(
`${chargingStation.logPrefix()} Remote starting transaction REJECTED on connector id
- ${connectorId.toString()}, idTag '${idTag}', availability '${chargingStation.getConnectorStatus(
- connectorId,
- )?.availability}', status '${chargingStation.getConnectorStatus(connectorId)?.status}'`,
+ ${connectorId}, idTag '${idTag}', availability '${connectorStatus?.availability}', status '${connectorStatus?.status}'`,
);
return OCPP16Constants.OCPP_RESPONSE_REJECTED;
}
connectorId: number,
chargingProfile: OCPP16ChargingProfile,
): boolean {
- if (
- chargingProfile &&
- chargingProfile.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE
- ) {
+ if (chargingProfile?.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE) {
OCPP16ServiceUtils.setChargingProfile(chargingStation, connectorId, chargingProfile);
logger.debug(
`${chargingStation.logPrefix()} Charging profile(s) set at remote start transaction
chargingProfile,
);
return true;
- } else if (
- chargingProfile &&
- chargingProfile.chargingProfilePurpose !== OCPP16ChargingProfilePurposeType.TX_PROFILE
- ) {
- logger.warn(
- `${chargingStation.logPrefix()} Not allowed to set ${
- chargingProfile.chargingProfilePurpose
- } charging profile(s) at remote start transaction`,
- );
- return false;
}
- return true;
+ logger.warn(
+ `${chargingStation.logPrefix()} Not allowed to set ${
+ chargingProfile.chargingProfilePurpose
+ } charging profile(s) at remote start transaction`,
+ );
+ return false;
}
private async handleRequestRemoteStopTransaction(
}
logger.warn(
`${chargingStation.logPrefix()} Trying to remote stop a non existing transaction with id
- ${transactionId.toString()}`,
+ ${transactionId}`,
);
return OCPP16Constants.OCPP_RESPONSE_REJECTED;
}
}
throw new OCPPError(
ErrorType.GENERIC_ERROR,
- `Diagnostics transfer failed with error code ${accessResponse.code.toString()}${
- uploadResponse?.code && `|${uploadResponse?.code.toString()}`
+ `Diagnostics transfer failed with error code ${accessResponse.code}${
+ uploadResponse?.code && `|${uploadResponse?.code}`
}`,
OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
);
}
throw new OCPPError(
ErrorType.GENERIC_ERROR,
- `Diagnostics transfer failed with error code ${accessResponse.code.toString()}${
- uploadResponse?.code && `|${uploadResponse?.code.toString()}`
+ `Diagnostics transfer failed with error code ${accessResponse.code}${
+ uploadResponse?.code && `|${uploadResponse?.code}`
}`,
OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
);