checkConnectorsConfiguration,
checkStationInfoConnectorStatus,
checkTemplate,
- countReservableConnectors,
createBootNotificationRequest,
createSerialNumber,
getAmperageLimitationUnitDivider,
getHashId,
getIdTagsFile,
getMaxNumberOfEvses,
+ getNumberOfReservableConnectors,
getPhaseRotationValue,
hasFeatureProfile,
+ hasReservationExpired,
initializeConnectorsMapStatus,
propagateSerialNumber,
+ removeExpiredReservations,
stationTemplateToStationInfo,
warnTemplateKeysDeprecation,
} from './Helpers';
public async removeReservation(
reservation: Reservation,
- reason?: ReservationTerminationReason,
+ reason: ReservationTerminationReason,
): Promise<void> {
const connector = this.getConnectorStatus(reservation.connectorId)!;
switch (reason) {
delete connector.reservation;
break;
default:
- break;
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ throw new Error(`Unknown reservation termination reason '${reason}'`);
}
}
idTag?: string,
connectorId?: number,
): boolean {
- const reservationExists = !isUndefined(this.getReservationBy('reservationId', reservationId));
+ const reservation = this.getReservationBy('reservationId', reservationId);
+ const reservationExists = !isUndefined(reservation) && !hasReservationExpired(reservation!);
if (arguments.length === 1) {
return !reservationExists;
} else if (arguments.length > 1) {
+ const userReservation = !isUndefined(idTag)
+ ? this.getReservationBy('idTag', idTag!)
+ : undefined;
const userReservationExists =
- !isUndefined(idTag) && isUndefined(this.getReservationBy('idTag', idTag!)) ? false : true;
+ !isUndefined(userReservation) && !hasReservationExpired(userReservation!);
const notConnectorZero = isUndefined(connectorId) ? true : connectorId! > 0;
const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0;
return (
)}`,
);
this.reservationExpirationSetInterval = setInterval((): void => {
- const currentDate = new Date();
- if (this.hasEvses) {
- for (const evseStatus of this.evses.values()) {
- for (const connectorStatus of evseStatus.connectors.values()) {
- if (
- connectorStatus.reservation &&
- connectorStatus.reservation.expiryDate < currentDate
- ) {
- this.removeReservation(
- connectorStatus.reservation,
- ReservationTerminationReason.EXPIRED,
- ).catch(Constants.EMPTY_FUNCTION);
- }
- }
- }
- } else {
- for (const connectorStatus of this.connectors.values()) {
- if (
- connectorStatus.reservation &&
- connectorStatus.reservation.expiryDate < currentDate
- ) {
- this.removeReservation(
- connectorStatus.reservation,
- ReservationTerminationReason.EXPIRED,
- ).catch(Constants.EMPTY_FUNCTION);
- }
- }
- }
+ removeExpiredReservations(this).catch(Constants.EMPTY_FUNCTION);
}, interval);
}
}
// }
private getNumberOfReservableConnectors(): number {
- let reservableConnectors = 0;
+ let numberOfReservableConnectors = 0;
if (this.hasEvses) {
for (const evseStatus of this.evses.values()) {
- reservableConnectors += countReservableConnectors(evseStatus.connectors);
+ numberOfReservableConnectors += getNumberOfReservableConnectors(evseStatus.connectors);
}
} else {
- reservableConnectors = countReservableConnectors(this.connectors);
+ numberOfReservableConnectors = getNumberOfReservableConnectors(this.connectors);
}
- return reservableConnectors - this.getNumberOfReservationsOnConnectorZero();
+ return numberOfReservableConnectors - this.getNumberOfReservationsOnConnectorZero();
}
private getNumberOfReservationsOnConnectorZero(): number {
isAfter,
isBefore,
isDate,
+ isPast,
isWithinInterval,
toDate,
} from 'date-fns';
type OCPP20BootNotificationRequest,
OCPPVersion,
RecurrencyKindType,
+ type Reservation,
+ ReservationTerminationReason,
StandardParametersKey,
SupportedFeatureProfiles,
Voltage,
)}${idSuffix}`;
};
-export const countReservableConnectors = (connectors: Map<number, ConnectorStatus>) => {
+export const hasReservationExpired = (reservation: Reservation): boolean => {
+ return isPast(reservation.expiryDate);
+};
+
+export const removeExpiredReservations = async (
+ chargingStation: ChargingStation,
+): Promise<void> => {
+ if (chargingStation.hasEvses) {
+ for (const evseStatus of chargingStation.evses.values()) {
+ for (const connectorStatus of evseStatus.connectors.values()) {
+ if (connectorStatus.reservation && hasReservationExpired(connectorStatus.reservation)) {
+ await chargingStation.removeReservation(
+ connectorStatus.reservation,
+ ReservationTerminationReason.EXPIRED,
+ );
+ }
+ }
+ }
+ } else {
+ for (const connectorStatus of chargingStation.connectors.values()) {
+ if (connectorStatus.reservation && hasReservationExpired(connectorStatus.reservation)) {
+ await chargingStation.removeReservation(
+ connectorStatus.reservation,
+ ReservationTerminationReason.EXPIRED,
+ );
+ }
+ }
+ }
+};
+
+export const getNumberOfReservableConnectors = (
+ connectors: Map<number, ConnectorStatus>,
+): number => {
let reservableConnectors = 0;
for (const [connectorId, connectorStatus] of connectors) {
if (connectorId === 0) {
this.cleanRequestPayload(command, requestPayload);
return this.commandHandlers.get(command)!(requestPayload);
}
- throw new BaseError(`Unknown worker broadcast channel command: ${command}`);
+ throw new BaseError(`Unknown worker broadcast channel command: '${command}'`);
}
private cleanRequestPayload(
setConfigurationKeyValue,
} from './ConfigurationKeyUtils';
export {
- getIdTagsFile,
checkChargingStation,
- resetConnectorStatus,
+ getIdTagsFile,
hasFeatureProfile,
+ hasReservationExpired,
+ removeExpiredReservations,
+ resetConnectorStatus,
} from './Helpers';
type ChargingStation,
checkChargingStation,
getConfigurationKey,
+ removeExpiredReservations,
setConfigurationKeyValue,
} from '../../../charging-station';
import { OCPPError } from '../../../exception';
idTag,
);
}
- if (
- (chargingStation.getConnectorStatus(transactionConnectorId)?.status ===
- OCPP16ChargePointStatus.Reserved &&
- chargingStation.getReservationBy('connectorId', transactionConnectorId)?.idTag !== idTag) ||
- (chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved &&
- chargingStation.getReservationBy('connectorId', 0)?.idTag !== idTag)
- ) {
- return OCPP16Constants.OCPP_RESPONSE_REJECTED;
- }
const remoteStartTransactionLogMsg = `
${chargingStation.logPrefix()} Transaction remotely STARTED on ${
chargingStation.stationInfo.chargingStationId
>(chargingStation, OCPP16RequestCommand.START_TRANSACTION, {
connectorId: transactionConnectorId,
idTag,
- reservationId: chargingStation.getReservationBy(
- 'connectorId',
- chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved
- ? 0
- : transactionConnectorId,
- )!,
})
).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED
) {
>(chargingStation, OCPP16RequestCommand.START_TRANSACTION, {
connectorId: transactionConnectorId,
idTag,
- reservationId: chargingStation.getReservationBy(
- 'connectorId',
- chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved
- ? 0
- : transactionConnectorId,
- )!,
})
).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED
) {
if (!(await OCPP16ServiceUtils.isIdTagAuthorized(chargingStation, connectorId, idTag))) {
return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED;
}
+ await removeExpiredReservations(chargingStation);
switch (chargingStation.getConnectorStatus(connectorId)!.status) {
case OCPP16ChargePointStatus.Faulted:
response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_FAULTED;
const { reservationId } = commandPayload;
const reservation = chargingStation.getReservationBy('reservationId', reservationId);
if (isUndefined(reservation)) {
- logger.error(
- `${chargingStation.logPrefix()} Reservation with ID ${reservationId}
+ logger.debug(
+ `${chargingStation.logPrefix()} Reservation with id ${reservationId}
does not exist on charging station`,
);
return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED;
type JsonType,
type OCPP16AuthorizeRequest,
type OCPP16BootNotificationRequest,
+ OCPP16ChargePointStatus,
type OCPP16DataTransferRequest,
type OCPP16DiagnosticsStatusNotificationRequest,
type OCPP16FirmwareStatusNotificationRequest,
true,
),
timestamp: new Date(),
+ ...(OCPP16ServiceUtils.hasReservation(
+ chargingStation,
+ commandParams?.connectorId as number,
+ commandParams?.idTag as string,
+ ) && {
+ reservationId: chargingStation.getReservationBy(
+ 'connectorId',
+ chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved
+ ? 0
+ : (commandParams?.connectorId as number),
+ )!.reservationId,
+ }),
...commandParams,
} as unknown as Request;
case OCPP16RequestCommand.STOP_TRANSACTION:
type ChargingStation,
addConfigurationKey,
getConfigurationKey,
+ hasReservationExpired,
resetConnectorStatus,
} from '../../../charging-station';
import { OCPPError } from '../../../exception';
transactionConnectorId,
requestPayload.meterStart,
);
- const reservedOnConnectorZero =
- chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved;
- if (
- chargingStation.getConnectorStatus(transactionConnectorId)?.status ===
- OCPP16ChargePointStatus.Reserved ||
- reservedOnConnectorZero
- ) {
+ if (requestPayload.reservationId) {
+ const reservation = chargingStation.getReservationBy(
+ 'reservationId',
+ requestPayload.reservationId,
+ )!;
+ if (reservation.idTag !== requestPayload.idTag) {
+ logger.warn(
+ `${chargingStation.logPrefix()} Transaction reserved ${
+ payload.transactionId
+ } started with a different idTag ${requestPayload.idTag} than the reservation one ${
+ reservation.idTag
+ }`,
+ );
+ }
+ if (hasReservationExpired(reservation)) {
+ logger.warn(
+ `${chargingStation.logPrefix()} Transaction reserved ${
+ payload.transactionId
+ } started with expired reservation ${
+ requestPayload.reservationId
+ } (expiry date: ${reservation.expiryDate.toISOString()}))`,
+ );
+ }
await chargingStation.removeReservation(
- chargingStation.getReservationBy(
- 'connectorId',
- reservedOnConnectorZero ? 0 : transactionConnectorId,
- )!,
+ reservation,
ReservationTerminationReason.TRANSACTION_STARTED,
);
}
import type { JSONSchemaType } from 'ajv';
import { OCPP16Constants } from './OCPP16Constants';
-import { type ChargingStation, hasFeatureProfile } from '../../../charging-station';
+import {
+ type ChargingStation,
+ hasFeatureProfile,
+ hasReservationExpired,
+} from '../../../charging-station';
import { OCPPError } from '../../../exception';
import {
type ClearChargingProfileRequest,
return clearedCP;
};
+ 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 &&
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ (hasReservationExpired(connectorReservation) || connectorReservation?.idTag !== idTag)) ||
+ (chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved &&
+ chargingStationReservation &&
+ (hasReservationExpired(chargingStationReservation) ||
+ chargingStationReservation?.idTag !== idTag))
+ ) {
+ return false;
+ }
+ return true;
+ };
+
public static parseJsonSchemaFile<T extends JsonType>(
relativePath: string,
moduleName?: string,
logger.error(
`${logPrefix(
' UI WebSocket Server |',
- )} Unsupported protocol: ${protocol} or protocol version: ${version}`,
+ )} Unsupported protocol: '${protocol}' or protocol version: '${version}'`,
);
return false;
};
/* This is intentional */
});
- static readonly DEFAULT_RESERVATION_EXPIRATION_OBSERVATION_INTERVAL = 5000; // Ms
+ static readonly DEFAULT_RESERVATION_EXPIRATION_OBSERVATION_INTERVAL = 60000; // Ms
private constructor() {
// This is intentional