// 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,
- type ConnectorStatus,
CurrentType,
ErrorType,
+ type GenericResponse,
type JsonType,
type MeasurandPerPhaseSampledValueTemplates,
type MeasurandValues,
MeterValueLocation,
MeterValueUnit,
OCPP16AuthorizationStatus,
- type OCPP16AuthorizeRequest,
- type OCPP16AuthorizeResponse,
+ OCPP16AvailabilityType,
+ type OCPP16ChangeAvailabilityResponse,
+ OCPP16ChargePointStatus,
type OCPP16ChargingProfile,
+ type OCPP16ChargingSchedule,
type OCPP16IncomingRequestCommand,
type OCPP16MeterValue,
OCPP16MeterValueMeasurand,
OCPP16RequestCommand,
type OCPP16SampledValue,
OCPP16StandardParametersKey,
+ OCPP16StopTransactionReason,
type OCPP16SupportedFeatureProfiles,
OCPPVersion,
type SampledValueTemplate,
DCElectricUtils,
convertToFloat,
convertToInt,
- formatDurationMilliSeconds,
getRandomFloatFluctuatedRounded,
getRandomFloatRounded,
getRandomInteger,
isNotEmptyArray,
- isNotEmptyString,
isNullOrUndefined,
isUndefined,
logger,
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
`${chargingStation.logPrefix()} MeterValues measurand ${
meterValue.sampledValue[sampledValuesIndex].measurand ??
OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
- }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${formatDurationMilliSeconds(
- interval,
- )}(${roundTo(interval, 4)}ms)`,
+ }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${interval}ms`,
);
}
}
return meterValues;
}
+ public static remoteStopTransaction = async (
+ chargingStation: ChargingStation,
+ connectorId: number,
+ ): Promise<GenericResponse> => {
+ 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<OCPP16ChangeAvailabilityResponse> => {
+ 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,
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 = [];
}
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 === commandPayload.id) {
+ if (chargingProfile.chargingProfileId === id) {
clearCurrentCP = true;
}
- if (
- !commandPayload.chargingProfilePurpose &&
- chargingProfile.stackLevel === commandPayload.stackLevel
- ) {
+ if (!chargingProfilePurpose && chargingProfile.stackLevel === stackLevel) {
clearCurrentCP = true;
}
- if (
- !chargingProfile.stackLevel &&
- chargingProfile.chargingProfilePurpose === commandPayload.chargingProfilePurpose
- ) {
+ if (!stackLevel && chargingProfile.chargingProfilePurpose === chargingProfilePurpose) {
clearCurrentCP = true;
}
if (
- chargingProfile.stackLevel === commandPayload.stackLevel &&
- chargingProfile.chargingProfilePurpose === commandPayload.chargingProfilePurpose
+ chargingProfile.stackLevel === stackLevel &&
+ chargingProfile.chargingProfilePurpose === chargingProfilePurpose
) {
clearCurrentCP = true;
}
return clearedCP;
};
+ public static composeChargingSchedules = (
+ chargingSchedule1: OCPP16ChargingSchedule | undefined,
+ chargingSchedule2: OCPP16ChargingSchedule | undefined,
+ targetInterval: Interval,
+ ): OCPP16ChargingSchedule | undefined => {
+ if (!chargingSchedule1 && !chargingSchedule2) {
+ return undefined;
+ }
+ if (chargingSchedule1 && !chargingSchedule2) {
+ return OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule1, targetInterval);
+ }
+ if (!chargingSchedule1 && chargingSchedule2) {
+ return OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule2, targetInterval);
+ }
+ const compositeChargingSchedule1: OCPP16ChargingSchedule | undefined =
+ OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule1!, targetInterval);
+ const compositeChargingSchedule2: OCPP16ChargingSchedule | undefined =
+ OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule2!, targetInterval);
+ const compositeChargingScheduleInterval1: Interval = {
+ start: compositeChargingSchedule1!.startSchedule!,
+ end: addSeconds(
+ compositeChargingSchedule1!.startSchedule!,
+ compositeChargingSchedule1!.duration!,
+ ),
+ };
+ const compositeChargingScheduleInterval2: Interval = {
+ start: compositeChargingSchedule2!.startSchedule!,
+ end: addSeconds(
+ compositeChargingSchedule2!.startSchedule!,
+ compositeChargingSchedule2!.duration!,
+ ),
+ };
+ if (
+ !areIntervalsOverlapping(
+ compositeChargingScheduleInterval1,
+ compositeChargingScheduleInterval2,
+ )
+ ) {
+ return {
+ ...OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule1!, targetInterval)!,
+ ...OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule2!, targetInterval)!,
+ };
+ }
+ // FIXME: Handle overlapping intervals
+ };
+
+ 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<T extends JsonType>(
relativePath: string,
moduleName?: string,
);
}
- public static async isIdTagAuthorized(
- chargingStation: ChargingStation,
- connectorId: number,
- idTag: string,
- ): Promise<boolean> {
- let authorized = false;
- const connectorStatus: 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,
+ targetInterval: Interval,
+ ): OCPP16ChargingSchedule | undefined => {
+ const chargingScheduleInterval: Interval = {
+ start: chargingSchedule.startSchedule!,
+ end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
+ };
+ if (areIntervalsOverlapping(chargingScheduleInterval, targetInterval)) {
+ chargingSchedule.chargingSchedulePeriod.sort((a, b) => a.startPeriod - b.startPeriod);
+ if (isBefore(chargingScheduleInterval.start, targetInterval.start)) {
+ return {
+ ...chargingSchedule,
+ startSchedule: targetInterval.start as Date,
+ duration: differenceInSeconds(chargingScheduleInterval.end, targetInterval.start as Date),
+ chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter(
+ (schedulePeriod, index) => {
+ if (
+ isWithinInterval(
+ addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
+ targetInterval,
+ )
+ ) {
+ return true;
+ }
+ if (
+ index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
+ !isWithinInterval(
+ addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod),
+ targetInterval,
+ ) &&
+ isWithinInterval(
+ addSeconds(
+ chargingScheduleInterval.start,
+ chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
+ ),
+ targetInterval,
+ )
+ ) {
+ schedulePeriod.startPeriod = 0;
+ return true;
+ }
+ return false;
+ },
+ ),
+ };
+ }
+ if (isAfter(chargingScheduleInterval.end, targetInterval.end)) {
+ return {
+ ...chargingSchedule,
+ duration: differenceInSeconds(targetInterval.end as Date, chargingScheduleInterval.start),
+ chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter((schedulePeriod) =>
+ isWithinInterval(
+ addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
+ targetInterval,
+ ),
+ ),
+ };
+ }
+ return chargingSchedule;
}
- return authorized;
- }
+ };
private static buildSampledValue(
sampledValueTemplate: SampledValueTemplate,
}
}
- 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<boolean> {
- 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;
+ // }
+ // }
}