#### Reservation Profile
-- :x: CancelReservation
-- :x: ReserveNow
+- :white_check_mark: CancelReservation
+- :white_check_mark: ReserveNow
#### Smart Charging Profile
{
"key": "SupportedFeatureProfiles",
"readonly": true,
- "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger"
+ "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation"
},
{
"key": "LocalAuthListEnabled",
"key": "WebSocketPingInterval",
"readonly": false,
"value": "60"
+ },
+ {
+ "key": "ReserveConnectorZeroSupported",
+ "readonly": false,
+ "value": "true"
}
]
},
{
"key": "SupportedFeatureProfiles",
"readonly": true,
- "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger"
+ "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation"
},
{
"key": "LocalAuthListEnabled",
"key": "WebSocketPingInterval",
"readonly": false,
"value": "60"
+ },
+ {
+ "key": "ReserveConnectorZeroSupported",
+ "readonly": false,
+ "value": "false"
}
]
},
{
"key": "SupportedFeatureProfiles",
"readonly": true,
- "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger"
+ "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation"
},
{
"key": "LocalAuthListEnabled",
"key": "WebSocketPingInterval",
"readonly": false,
"value": "60"
+ },
+ {
+ "key": "ReserveConnectorZeroSupported",
+ "readonly": false,
+ "value": "true"
}
]
},
{
"key": "SupportedFeatureProfiles",
"readonly": true,
- "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger"
+ "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation"
},
{
"key": "LocalAuthListEnabled",
"key": "WebSocketPingInterval",
"readonly": false,
"value": "60"
+ },
+ {
+ "key": "ReserveConnectorZeroSupported",
+ "readonly": false,
+ "value": "false"
}
]
},
{
"key": "SupportedFeatureProfiles",
"readonly": true,
- "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger"
+ "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation"
},
{
"key": "LocalAuthListEnabled",
"key": "WebSocketPingInterval",
"readonly": false,
"value": "60"
+ },
+ {
+ "key": "ReserveConnectorZeroSupported",
+ "readonly": false,
+ "value": "true"
}
]
},
{
"key": "SupportedFeatureProfiles",
"readonly": true,
- "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger"
+ "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation"
},
{
"key": "LocalAuthListEnabled",
"key": "WebSocketPingInterval",
"readonly": false,
"value": "60"
+ },
+ {
+ "key": "ReserveConnectorZeroSupported",
+ "readonly": false,
+ "value": "false"
}
]
},
{
"key": "SupportedFeatureProfiles",
"readonly": true,
- "value": "Core,LocalAuthListManagement"
+ "value": "Core,LocalAuthListManagement,Reservation"
},
{
"key": "LocalAuthListEnabled",
"key": "WebSocketPingInterval",
"readonly": false,
"value": "60"
+ },
+ {
+ "key": "ReserveConnectorZeroSupported",
+ "readonly": false,
+ "value": "true"
}
]
},
{
"key": "SupportedFeatureProfiles",
"readonly": true,
- "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger"
+ "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation"
},
{
"key": "LocalAuthListEnabled",
"key": "WebSocketPingInterval",
"readonly": false,
"value": "60"
+ },
+ {
+ "key": "ReserveConnectorZeroSupported",
+ "readonly": false,
+ "value": "false"
}
]
},
{
"key": "SupportedFeatureProfiles",
"readonly": true,
- "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger"
+ "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation"
},
{
"key": "LocalAuthListEnabled",
"key": "WebSocketPingInterval",
"readonly": false,
"value": "60"
+ },
+ {
+ "key": "ReserveConnectorZeroSupported",
+ "readonly": false,
+ "value": "true"
}
]
},
{
"key": "SupportedFeatureProfiles",
"readonly": true,
- "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger"
+ "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation"
},
{
"key": "LocalAuthListEnabled",
type OCPPRequestService,
OCPPServiceUtils,
} from './ocpp';
+import { OCPPConstants } from './ocpp/OCPPConstants';
import { SharedLRUCache } from './SharedLRUCache';
import { BaseError, OCPPError } from '../exception';
import { PerformanceStatistics } from '../performance';
MeterValueMeasurand,
type MeterValuesRequest,
type MeterValuesResponse,
+ OCPP16AuthorizationStatus,
+ type OCPP16AuthorizeRequest,
+ type OCPP16AuthorizeResponse,
+ OCPP16RequestCommand,
+ OCPP16SupportedFeatureProfiles,
OCPPVersion,
type OutgoingRequest,
PowerUnits,
WebSocketCloseEventStatusCode,
type WsOptions,
} from '../types';
+import { ReservationTerminationReason } from '../types/ocpp/1.6/Reservation';
+import type { Reservation } from '../types/ocpp/Reservation';
import {
ACElectricUtils,
AsyncLock,
private readonly sharedLRUCache: SharedLRUCache;
private webSocketPingSetInterval!: NodeJS.Timeout;
private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel;
+ private reservations?: Reservation[];
+ private reservationExpiryDateSetInterval?: NodeJS.Timeout;
constructor(index: number, templateFile: string) {
this.started = false;
if (this.getEnableStatistics() === true) {
this.performanceStatistics?.start();
}
+ if (this.supportsReservations()) {
+ this.startReservationExpiryDateSetInterval();
+ }
this.openWSConnection();
// Monitor charging station template file
this.templateFileWatcher = FileUtils.watchJsonFile(
);
}
+ public supportsReservations(): boolean {
+ logger.info(`${this.logPrefix()} Check for reservation support in charging station`);
+ return ChargingStationConfigurationUtils.getConfigurationKey(
+ this,
+ StandardParametersKey.SupportedFeatureProfiles
+ ).value.includes(OCPP16SupportedFeatureProfiles.Reservation);
+ }
+
+ public supportsReservationsOnConnectorId0(): boolean {
+ logger.info(
+ ` ${this.logPrefix()} Check for reservation support on connector 0 in charging station (CS)`
+ );
+ return (
+ this.supportsReservations() &&
+ ChargingStationConfigurationUtils.getConfigurationKey(
+ this,
+ OCPPConstants.OCPP_RESERVE_CONNECTOR_ZERO_SUPPORTED
+ ).value === 'true'
+ );
+ }
+
+ public async addReservation(reservation: Reservation): Promise<void> {
+ if (Utils.isNullOrUndefined(this.reservations)) {
+ this.reservations = [];
+ }
+ const [exists, reservationFound] = this.doesReservationExists(reservation);
+ if (exists) {
+ await this.removeReservation(reservationFound);
+ }
+ this.reservations.push(reservation);
+ if (reservation.connectorId === 0) {
+ return;
+ }
+ this.getConnectorStatus(reservation.connectorId).status = ConnectorStatusEnum.Reserved;
+ await this.ocppRequestService.requestHandler<
+ StatusNotificationRequest,
+ StatusNotificationResponse
+ >(
+ this,
+ RequestCommand.STATUS_NOTIFICATION,
+ OCPPServiceUtils.buildStatusNotificationRequest(
+ this,
+ reservation.connectorId,
+ ConnectorStatusEnum.Reserved
+ )
+ );
+ }
+
+ public async removeReservation(
+ reservation: Reservation,
+ reason?: ReservationTerminationReason
+ ): Promise<void> {
+ const sameReservation = (r: Reservation) => r.id === reservation.id;
+ const index = this.reservations?.findIndex(sameReservation);
+ this.reservations.splice(index, 1);
+ switch (reason) {
+ case ReservationTerminationReason.TRANSACTION_STARTED:
+ // No action needed
+ break;
+ case ReservationTerminationReason.CONNECTOR_STATE_CHANGED:
+ // No action needed
+ break;
+ default: // ReservationTerminationReason.EXPIRED, ReservationTerminationReason.CANCELED
+ this.getConnectorStatus(reservation.connectorId).status = ConnectorStatusEnum.Available;
+ await this.ocppRequestService.requestHandler<
+ StatusNotificationRequest,
+ StatusNotificationResponse
+ >(
+ this,
+ RequestCommand.STATUS_NOTIFICATION,
+ OCPPServiceUtils.buildStatusNotificationRequest(
+ this,
+ reservation.connectorId,
+ ConnectorStatusEnum.Available
+ )
+ );
+ break;
+ }
+ }
+
+ public getReservationById(id: number): Reservation {
+ return this.reservations?.find((reservation) => reservation.id === id);
+ }
+
+ public getReservationByIdTag(id: string): Reservation {
+ return this.reservations?.find((reservation) => reservation.idTag === id);
+ }
+
+ public getReservationByConnectorId(id: number): Reservation {
+ return this.reservations?.find((reservation) => reservation.connectorId === id);
+ }
+
+ public doesReservationExists(reservation: Partial<Reservation>): [boolean, Reservation] {
+ const sameReservation = (r: Reservation) => r.id === reservation.id;
+ const foundReservation = this.reservations?.find(sameReservation);
+ return Utils.isUndefined(foundReservation) ? [false, null] : [true, foundReservation];
+ }
+
+ public async isAuthorized(
+ connectorId: number,
+ idTag: string,
+ parentIdTag?: string
+ ): Promise<boolean> {
+ let authorized = false;
+ const connectorStatus = this.getConnectorStatus(connectorId);
+ if (
+ this.getLocalAuthListEnabled() === true &&
+ this.hasIdTags() === true &&
+ Utils.isNotEmptyString(
+ this.idTagsCache
+ .getIdTags(ChargingStationUtils.getIdTagsFile(this.stationInfo))
+ ?.find((tag) => tag === idTag)
+ )
+ ) {
+ connectorStatus.localAuthorizeIdTag = idTag;
+ connectorStatus.idTagLocalAuthorized = true;
+ authorized = true;
+ } else if (this.getMustAuthorizeAtRemoteStart() === true) {
+ connectorStatus.authorizeIdTag = idTag;
+ const authorizeResponse: OCPP16AuthorizeResponse =
+ await this.ocppRequestService.requestHandler<
+ OCPP16AuthorizeRequest,
+ OCPP16AuthorizeResponse
+ >(this, OCPP16RequestCommand.AUTHORIZE, {
+ idTag: idTag,
+ });
+ if (authorizeResponse?.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
+ authorized = true;
+ }
+ } else {
+ logger.warn(
+ `${this.logPrefix()} The charging station configuration expects authorize at
+ remote start transaction but local authorization or authorize isn't enabled`
+ );
+ }
+ return authorized;
+ }
+
+ public startReservationExpiryDateSetInterval(customInterval?: number): void {
+ const interval =
+ customInterval ?? Constants.DEFAULT_RESERVATION_EXPIRATION_OBSERVATION_INTERVAL;
+ logger.info(
+ `${this.logPrefix()} Reservation expiration date interval is set to ${interval}
+ and starts on CS now`
+ );
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ this.reservationExpiryDateSetInterval = setInterval(async (): Promise<void> => {
+ if (!Utils.isNullOrUndefined(this.reservations) && !Utils.isEmptyArray(this.reservations)) {
+ for (const reservation of this.reservations) {
+ if (reservation.expiryDate.toString() < new Date().toISOString()) {
+ await this.removeReservation(reservation);
+ logger.info(
+ `${this.logPrefix()} Reservation with ID ${
+ reservation.id
+ } reached expiration date and was removed from CS`
+ );
+ }
+ }
+ }
+ }, interval);
+ }
+
+ public restartReservationExpiryDateSetInterval(): void {
+ this.stopReservationExpiryDateSetInterval();
+ this.startReservationExpiryDateSetInterval();
+ }
+
+ public validateIncomingRequestWithReservation(connectorId: number, idTag: string): boolean {
+ const reservation = this.getReservationByConnectorId(connectorId);
+ return Utils.isUndefined(reservation) || reservation.idTag !== idTag;
+ }
+
+ public isConnectorReservable(
+ reservationId: number,
+ connectorId?: number,
+ idTag?: string
+ ): boolean {
+ const [alreadyExists] = this.doesReservationExists({ id: reservationId });
+ if (alreadyExists) {
+ return alreadyExists;
+ }
+ const userReservedAlready = Utils.isUndefined(this.getReservationByIdTag(idTag)) ? false : true;
+ const notConnectorZero = Utils.isUndefined(connectorId) ? true : connectorId > 0;
+ const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0;
+ return !alreadyExists && !userReservedAlready && notConnectorZero && freeConnectorsAvailable;
+ }
+
+ private getNumberOfReservableConnectors(): number {
+ let reservableConnectors = 0;
+ this.connectors.forEach((connector, id) => {
+ if (id === 0) {
+ return;
+ }
+ if (connector.status === ConnectorStatusEnum.Available) {
+ reservableConnectors++;
+ }
+ });
+ return reservableConnectors - this.getNumberOfReservationsOnConnectorZero();
+ }
+
+ private getNumberOfReservationsOnConnectorZero(): number {
+ const reservations = this.reservations?.filter((reservation) => reservation.connectorId === 0);
+ return Utils.isNullOrUndefined(reservations) ? 0 : reservations.length;
+ }
+
private flushMessageBuffer(): void {
if (this.messageBuffer.size > 0) {
for (const message of this.messageBuffer.values()) {
return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
}
+ private stopReservationExpiryDateSetInterval(): void {
+ if (this.reservationExpiryDateSetInterval) {
+ clearInterval(this.reservationExpiryDateSetInterval);
+ }
+ }
+
private getSupervisionUrlOcppKey(): string {
return this.stationInfo.supervisionUrlOcppKey ?? VendorParametersKey.ConnectionUrl;
}
type ClearChargingProfileRequest,
type ClearChargingProfileResponse,
type ConnectorStatus,
+ ConnectorStatusEnum,
ErrorType,
type GenericResponse,
GenericStatus,
type JsonObject,
type JsonType,
OCPP16AuthorizationStatus,
- type OCPP16AuthorizeRequest,
- type OCPP16AuthorizeResponse,
OCPP16AvailabilityType,
type OCPP16BootNotificationRequest,
type OCPP16BootNotificationResponse,
type UnlockConnectorRequest,
type UnlockConnectorResponse,
} from '../../../types';
+import type {
+ OCPP16CancelReservationRequest,
+ OCPP16ReserveNowRequest,
+} from '../../../types/ocpp/1.6/Requests';
+import { ReservationTerminationReason } from '../../../types/ocpp/1.6/Reservation';
+import type {
+ OCPP16CancelReservationResponse,
+ OCPP16ReserveNowResponse,
+} from '../../../types/ocpp/1.6/Responses';
import { Constants, Utils, logger } from '../../../utils';
+import { OCPPConstants } from '../OCPPConstants';
import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService';
const moduleName = 'OCPP16IncomingRequestService';
[OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, this.handleRequestTriggerMessage.bind(this)],
[OCPP16IncomingRequestCommand.DATA_TRANSFER, this.handleRequestDataTransfer.bind(this)],
[OCPP16IncomingRequestCommand.UPDATE_FIRMWARE, this.handleRequestUpdateFirmware.bind(this)],
+ [OCPP16IncomingRequestCommand.RESERVE_NOW, this.handleRequestReserveNow.bind(this)],
+ [
+ OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
+ this.handleRequestCancelReservation.bind(this),
+ ],
]);
this.jsonSchemas = new Map<OCPP16IncomingRequestCommand, JSONSchemaType<JsonObject>>([
[
'constructor'
),
],
+ [
+ OCPP16IncomingRequestCommand.RESERVE_NOW,
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ReserveNowRequest>(
+ 'assets/json-schemas/ocpp/1.6/ReserveNow.json',
+ moduleName,
+ 'constructor'
+ ),
+ ],
+ [
+ OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16CancelReservationRequest>(
+ 'assets/json-schemas/ocpp/1.6/CancelReservation.json',
+ moduleName,
+ 'constructor'
+ ),
+ ],
]);
this.validatePayload = this.validatePayload.bind(this) as (
chargingStation: ChargingStation,
commandPayload: RemoteStartTransactionRequest
): Promise<GenericResponse> {
const transactionConnectorId = commandPayload.connectorId;
+ const reserved =
+ chargingStation.getConnectorStatus(transactionConnectorId).status ===
+ OCPP16ChargePointStatus.Reserved;
+ if (
+ reserved &&
+ chargingStation.validateIncomingRequestWithReservation(
+ transactionConnectorId,
+ commandPayload.idTag
+ )
+ ) {
+ return OCPP16Constants.OCPP_RESPONSE_REJECTED;
+ }
if (chargingStation.hasConnector(transactionConnectorId) === false) {
return this.notifyRemoteStartTransactionRejected(
chargingStation,
);
}
if (
- chargingStation.isChargingStationAvailable() === false ||
- chargingStation.isConnectorAvailable(transactionConnectorId) === false
+ !chargingStation.isChargingStationAvailable() ||
+ !chargingStation.isConnectorAvailable(transactionConnectorId)
) {
return this.notifyRemoteStartTransactionRejected(
chargingStation,
const connectorStatus = chargingStation.getConnectorStatus(transactionConnectorId);
// Check if authorized
if (chargingStation.getAuthorizeRemoteTxRequests() === true) {
- let authorized = false;
- if (
- chargingStation.getLocalAuthListEnabled() === true &&
- chargingStation.hasIdTags() === true &&
- Utils.isNotEmptyString(
- chargingStation.idTagsCache
- .getIdTags(ChargingStationUtils.getIdTagsFile(chargingStation.stationInfo))
- ?.find((idTag) => idTag === commandPayload.idTag)
- )
- ) {
- connectorStatus.localAuthorizeIdTag = commandPayload.idTag;
- connectorStatus.idTagLocalAuthorized = true;
- authorized = true;
- } else if (chargingStation.getMustAuthorizeAtRemoteStart() === true) {
- connectorStatus.authorizeIdTag = commandPayload.idTag;
- const authorizeResponse: OCPP16AuthorizeResponse =
- await chargingStation.ocppRequestService.requestHandler<
- OCPP16AuthorizeRequest,
- OCPP16AuthorizeResponse
- >(chargingStation, OCPP16RequestCommand.AUTHORIZE, {
- idTag: commandPayload.idTag,
- });
- if (authorizeResponse?.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
- authorized = true;
- }
- } else {
- logger.warn(
- `${chargingStation.logPrefix()} The charging station configuration expects authorize at remote start transaction but local authorization or authorize isn't enabled`
- );
- }
+ const authorized = await chargingStation.isAuthorized(
+ transactionConnectorId,
+ commandPayload.idTag
+ );
if (authorized === true) {
// Authorization successful, start transaction
if (
) === true
) {
connectorStatus.transactionRemoteStarted = true;
+ const startTransactionPayload: JsonType = {
+ connectorId: transactionConnectorId,
+ idTag: commandPayload.idTag,
+ };
+ if (reserved) {
+ const reservation = chargingStation.getReservationByConnectorId(transactionConnectorId);
+ startTransactionData.reservationId = reservation.id;
+ await chargingStation.removeReservation(
+ reservation,
+ ReservationTerminationReason.TRANSACTION_STARTED
+ );
+ }
if (
(
await chargingStation.ocppRequestService.requestHandler<
OCPP16StartTransactionRequest,
OCPP16StartTransactionResponse
- >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, {
- connectorId: transactionConnectorId,
- idTag: commandPayload.idTag,
- })
+ >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, startTransactionData)
).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED
) {
logger.debug(remoteStartTransactionLogMsg);
);
}
}
+
+ private async handleRequestReserveNow(
+ chargingStation: ChargingStation,
+ commandPayload: OCPP16ReserveNowRequest
+ ): Promise<OCPP16ReserveNowResponse> {
+ const { reservationId, idTag, connectorId } = commandPayload;
+ let response: OCPP16ReserveNowResponse;
+ try {
+ if (
+ !chargingStation.supportsReservations() &&
+ chargingStation.isConnectorAvailable(connectorId)
+ ) {
+ return OCPPConstants.OCPP_RESERVATION_RESPONSE_REJECTED;
+ }
+ if (connectorId === 0 && !chargingStation.supportsReservationsOnConnectorId0()) {
+ return OCPPConstants.OCPP_RESERVATION_RESPONSE_REJECTED;
+ }
+ if (!(await chargingStation.isAuthorized(connectorId, idTag))) {
+ return OCPPConstants.OCPP_RESERVATION_RESPONSE_REJECTED;
+ }
+ switch (chargingStation.getConnectorStatus(connectorId).status) {
+ case ConnectorStatusEnum.Faulted:
+ response = OCPPConstants.OCPP_RESERVATION_RESPONSE_FAULTED;
+ break;
+ case ConnectorStatusEnum.Occupied:
+ response = OCPPConstants.OCPP_RESERVATION_RESPONSE_OCCUPIED;
+ break;
+ case ConnectorStatusEnum.Unavailable:
+ response = OCPPConstants.OCPP_RESERVATION_RESPONSE_UNAVAILABLE;
+ break;
+ case ConnectorStatusEnum.Reserved:
+ if (!chargingStation.isConnectorReservable(reservationId, connectorId, idTag)) {
+ response = OCPPConstants.OCPP_RESERVATION_RESPONSE_OCCUPIED;
+ break;
+ }
+ // eslint-disable-next-line no-fallthrough
+ default:
+ if (!chargingStation.isConnectorReservable(reservationId)) {
+ response = OCPPConstants.OCPP_RESERVATION_RESPONSE_OCCUPIED;
+ break;
+ }
+ await chargingStation.addReservation({
+ id: commandPayload.reservationId,
+ ...commandPayload,
+ });
+ response = OCPPConstants.OCPP_RESERVATION_RESPONSE_ACCEPTED;
+ break;
+ }
+ return response;
+ } catch (error) {
+ chargingStation.getConnectorStatus(connectorId).status = ConnectorStatusEnum.Available;
+ return this.handleIncomingRequestError(
+ chargingStation,
+ OCPP16IncomingRequestCommand.RESERVE_NOW,
+ error as Error,
+ { errorResponse: OCPPConstants.OCPP_RESERVATION_RESPONSE_FAULTED }
+ );
+ }
+ }
+
+ private async handleRequestCancelReservation(
+ chargingStation: ChargingStation,
+ commandPayload: OCPP16CancelReservationRequest
+ ): Promise<OCPP16CancelReservationResponse> {
+ try {
+ const { reservationId } = commandPayload;
+ const [exists, reservation] = chargingStation.doesReservationExists({ id: reservationId });
+ if (!exists) {
+ logger.error(
+ `${chargingStation.logPrefix()} Reservation with ID ${reservationId} does not exist on charging station`
+ );
+ return OCPPConstants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED;
+ }
+ await chargingStation.removeReservation(reservation);
+ return OCPPConstants.OCPP_CANCEL_RESERVATION_RESPONSE_ACCEPTED;
+ } catch (error) {
+ return this.handleIncomingRequestError(
+ chargingStation,
+ OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
+ error as Error,
+ { errorResponse: OCPPConstants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED }
+ );
+ }
+ }
}
OCPPVersion,
type RequestParams,
} from '../../../types';
+import type {
+ OCPP16CancelReservationRequest,
+ OCPP16ReserveNowRequest,
+} from '../../../types/ocpp/1.6/Requests';
import { Constants, Utils } from '../../../utils';
import { OCPPRequestService } from '../OCPPRequestService';
import type { OCPPResponseService } from '../OCPPResponseService';
'constructor'
),
],
+ [
+ OCPP16RequestCommand.RESERVE_NOW,
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ReserveNowRequest>(
+ 'assets/json-schemas/ocpp/1.6/ReserveNow.json',
+ moduleName,
+ 'constructor'
+ ),
+ ],
+ [
+ OCPP16RequestCommand.CANCEL_RESERVATION,
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16CancelReservationRequest>(
+ 'assets/json-schemas/ocpp/1.6/CancelReservation.json',
+ moduleName,
+ 'constructor'
+ ),
+ ],
]);
this.buildRequestPayload = this.buildRequestPayload.bind(this) as <Request extends JsonType>(
chargingStation: ChargingStation,
TriggerMessageStatus,
UnlockStatus,
} from '../../types';
+import { CancelReservationStatus, ReservationStatus } from '../../types/ocpp/Responses';
import { Constants } from '../../utils';
export class OCPPConstants {
status: DataTransferStatus.REJECTED,
});
+ static readonly OCPP_RESERVATION_RESPONSE_ACCEPTED = Object.freeze({ status: ReservationStatus.ACCEPTED }); // Reservation has been made
+ static readonly OCPP_RESERVATION_RESPONSE_FAULTED = Object.freeze({ status: ReservationStatus.FAULTED }); // Reservation has not been made, because of connector in FAULTED state
+ static readonly OCPP_RESERVATION_RESPONSE_OCCUPIED = Object.freeze({ status: ReservationStatus.OCCUPIED }); // Reservation has not been made, because all connectors are OCCUPIED
+ static readonly OCPP_RESERVATION_RESPONSE_REJECTED = Object.freeze({ status: ReservationStatus.REJECTED }); // Reservation has not been made, because CS is not configured to accept reservations
+ static readonly OCPP_RESERVATION_RESPONSE_UNAVAILABLE = Object.freeze({ status: ReservationStatus.UNAVAILABLE }); // Reservation has not been made, because connectors are spec. connector is in UNAVAILABLE state
+
+ static readonly OCPP_CANCEL_RESERVATION_RESPONSE_ACCEPTED = Object.freeze({ status: CancelReservationStatus.ACCEPTED }); // Reservation for id has been cancelled has been made
+ static readonly OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED = Object.freeze({ status: CancelReservationStatus.REJECTED }); // Reservation could not be cancelled, because there is no reservation active for id
+
+ static readonly OCPP_SUPPORTED_FEATURE_PROFILE_RESERVATION = 'Reservation';
+ static readonly OCPP_RESERVE_CONNECTOR_ZERO_SUPPORTED = 'ReserveConnectorZeroSupported';
+
protected constructor() {
// This is intentional
}
MessageTrigger,
RequestCommand,
} from './ocpp/Requests';
+import type { Reservation } from './ocpp/Reservation';
export enum CurrentType {
AC = 'AC',
AutomaticTransactionGenerator?: AutomaticTransactionGeneratorConfiguration;
Evses?: Record<string, EvseTemplate>;
Connectors?: Record<string, ConnectorStatus>;
+ reservation?: Reservation[];
};
DIAGNOSTICS_STATUS_NOTIFICATION = 'DiagnosticsStatusNotification',
FIRMWARE_STATUS_NOTIFICATION = 'FirmwareStatusNotification',
DATA_TRANSFER = 'DataTransfer',
+ RESERVE_NOW = 'ReserveNow',
+ CANCEL_RESERVATION = 'CancelReservation',
}
export enum OCPP16IncomingRequestCommand {
TRIGGER_MESSAGE = 'TriggerMessage',
DATA_TRANSFER = 'DataTransfer',
UPDATE_FIRMWARE = 'UpdateFirmware',
+ RESERVE_NOW = 'ReserveNow',
+ CANCEL_RESERVATION = 'CancelReservation',
}
export type OCPP16HeartbeatRequest = EmptyObject;
messageId?: string;
data?: string;
}
+
+export interface OCPP16ReserveNowRequest extends JsonObject {
+ connectorId: number;
+ expiryDate: Date;
+ idTag: string;
+ parentIdTag?: string;
+ reservationId: number;
+}
+
+export interface OCPP16CancelReservationRequest extends JsonObject {
+ reservationId: number;
+}
--- /dev/null
+export interface OCPP16Reservation {
+ id: number;
+ connectorId: number;
+ expiryDate: Date;
+ idTag: string;
+ parentIdTag?: string;
+}
+
+export enum ReservationTerminationReason {
+ EXPIRED = 'Expired',
+ TRANSACTION_STARTED = 'TransactionStarted',
+ CONNECTOR_STATE_CHANGED = 'ConnectorStateChanged',
+ CANCELED = 'ReservationCanceled',
+}
status: OCPP16DataTransferStatus;
data?: string;
}
+
+export enum OCPP16CancelReservationStatus {
+ ACCEPTED = 'Accepted',
+ REJECTED = 'Rejected',
+}
+
+export interface OCPP16CancelReservationResponse {
+ status: OCPP16CancelReservationStatus;
+}
+
+export enum OCPP16ReservationStatus {
+ ACCEPTED = 'Accepted',
+ FAULTED = 'Faulted',
+ OCCUPIED = 'Occupied',
+ REJECTED = 'Rejected',
+ UNAVAILABLE = 'Unavailable',
+}
+
+export interface OCPP16ReserveNowResponse {
+ status: OCPP16ReservationStatus;
+}
--- /dev/null
+import { type OCPP16Reservation } from './1.6/Reservation';
+
+export type Reservation = OCPP16Reservation;
import {
OCPP16AvailabilityStatus,
type OCPP16BootNotificationResponse,
+ OCPP16CancelReservationStatus,
OCPP16ChargingProfileStatus,
OCPP16ClearChargingProfileStatus,
OCPP16ConfigurationStatus,
type OCPP16DiagnosticsStatusNotificationResponse,
type OCPP16FirmwareStatusNotificationResponse,
type OCPP16HeartbeatResponse,
+ OCPP16ReservationStatus,
type OCPP16StatusNotificationResponse,
OCPP16TriggerMessageStatus,
OCPP16UnlockStatus,
...OCPP16DataTransferStatus,
} as const;
export type DataTransferStatus = OCPP16DataTransferStatus;
+
+export type ReservationStatus = OCPP16ReservationStatus;
+export const ReservationStatus = {
+ ...OCPP16ReservationStatus,
+};
+
+export type CancelReservationStatus = OCPP16CancelReservationStatus;
+export const CancelReservationStatus = {
+ ...OCPP16CancelReservationStatus,
+};
/* This is intentional */
});
+ static readonly DEFAULT_RESERVATION_EXPIRATION_OBSERVATION_INTERVAL = 5000; // Ms
+
private constructor() {
// This is intentional
}