feat: add initial support get composite schedule OCPP 1.6 command
authorJérôme Benoit <jerome.benoit@sap.com>
Mon, 27 Mar 2023 18:24:03 +0000 (20:24 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Mon, 27 Mar 2023 18:24:03 +0000 (20:24 +0200)
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
src/charging-station/ChargingStationUtils.ts
src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts
src/charging-station/ocpp/1.6/OCPP16ResponseService.ts
src/types/index.ts
src/types/ocpp/1.6/ChargingProfile.ts
src/types/ocpp/1.6/Requests.ts
src/types/ocpp/1.6/Responses.ts

index 0c5addcc5fd6a20a89d8287feef1349fecf66d7f..1059a1d0c8b4da1215eb1184b1a25614ed32b07e 100644 (file)
@@ -329,11 +329,12 @@ export class ChargingStationUtils {
     connectorId: number
   ): number | undefined {
     let limit: number, matchingChargingProfile: ChargingProfile;
-    let chargingProfiles: ChargingProfile[] = [];
     // Get charging profiles for connector and sort by stack level
-    chargingProfiles = chargingStation
-      .getConnectorStatus(connectorId)
-      ?.chargingProfiles?.sort((a, b) => b.stackLevel - a.stackLevel);
+    const chargingProfiles = Utils.cloneObject(
+      chargingStation
+        .getConnectorStatus(connectorId)
+        ?.chargingProfiles?.sort((a, b) => b.stackLevel - a.stackLevel) ?? []
+    );
     // Get profiles on connector 0
     if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
       chargingProfiles.push(
@@ -459,10 +460,16 @@ export class ChargingStationUtils {
     matchingChargingProfile: ChargingProfile;
   } | null {
     const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
+    const currentMoment = moment();
+    const currentDate = new Date();
     for (const chargingProfile of chargingProfiles) {
       // Set helpers
-      const currentMoment = moment();
       const chargingSchedule = chargingProfile.chargingSchedule;
+      if (!chargingSchedule?.startSchedule) {
+        logger.warn(
+          `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}`
+        );
+      }
       // Check type (recurring) and if it is already active
       // Adjust the daily recurring schedule to today
       if (
@@ -470,8 +477,12 @@ export class ChargingStationUtils {
         chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
         currentMoment.isAfter(chargingSchedule.startSchedule)
       ) {
-        const currentDate = new Date();
-        chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule);
+        if (!(chargingSchedule?.startSchedule instanceof Date)) {
+          logger.warn(
+            `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object`
+          );
+          chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule);
+        }
         chargingSchedule.startSchedule.setFullYear(
           currentDate.getFullYear(),
           currentDate.getMonth(),
index cad78436513906e8ee0e42ab0be7b309a7933f24..999e1ceafaf9a703d81bcf2890e69eec728e3c0a 100644 (file)
@@ -23,6 +23,7 @@ import {
   type ClearChargingProfileResponse,
   ErrorType,
   type GenericResponse,
+  GenericStatus,
   type GetConfigurationRequest,
   type GetConfigurationResponse,
   type GetDiagnosticsRequest,
@@ -40,6 +41,7 @@ import {
   OCPP16ChargePointStatus,
   type OCPP16ChargingProfile,
   OCPP16ChargingProfilePurposeType,
+  type OCPP16ChargingSchedule,
   type OCPP16ClearCacheRequest,
   type OCPP16DataTransferRequest,
   type OCPP16DataTransferResponse,
@@ -51,6 +53,8 @@ import {
   OCPP16FirmwareStatus,
   type OCPP16FirmwareStatusNotificationRequest,
   type OCPP16FirmwareStatusNotificationResponse,
+  type OCPP16GetCompositeScheduleRequest,
+  type OCPP16GetCompositeScheduleResponse,
   type OCPP16HeartbeatRequest,
   type OCPP16HeartbeatResponse,
   OCPP16IncomingRequestCommand,
@@ -103,6 +107,10 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
         OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
         this.handleRequestChangeConfiguration.bind(this),
       ],
+      [
+        OCPP16IncomingRequestCommand.GET_COMPOSITE_SCHEDULE,
+        this.handleRequestGetCompositeSchedule.bind(this),
+      ],
       [
         OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
         this.handleRequestSetChargingProfile.bind(this),
@@ -177,6 +185,14 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
           'constructor'
         ),
       ],
+      [
+        OCPP16IncomingRequestCommand.GET_COMPOSITE_SCHEDULE,
+        OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16GetCompositeScheduleRequest>(
+          '../../../assets/json-schemas/ocpp/1.6/GetCompositeSchedule.json',
+          moduleName,
+          'constructor'
+        ),
+      ],
       [
         OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
         OCPP16ServiceUtils.parseJsonSchemaFile<SetChargingProfileRequest>(
@@ -568,6 +584,56 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     return OCPPConstants.OCPP_SET_CHARGING_PROFILE_RESPONSE_ACCEPTED;
   }
 
+  private handleRequestGetCompositeSchedule(
+    chargingStation: ChargingStation,
+    commandPayload: OCPP16GetCompositeScheduleRequest
+  ): OCPP16GetCompositeScheduleResponse {
+    if (
+      OCPP16ServiceUtils.checkFeatureProfile(
+        chargingStation,
+        OCPP16SupportedFeatureProfiles.SmartCharging,
+        OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE
+      ) === false
+    ) {
+      return OCPPConstants.OCPP_RESPONSE_REJECTED;
+    }
+    if (chargingStation.connectors.has(commandPayload.connectorId) === false) {
+      logger.error(
+        `${chargingStation.logPrefix()} Trying to get composite schedule to a non existing connector Id ${
+          commandPayload.connectorId
+        }`
+      );
+      return OCPPConstants.OCPP_RESPONSE_REJECTED;
+    }
+    if (
+      Utils.isEmptyArray(
+        chargingStation.getConnectorStatus(commandPayload.connectorId)?.chargingProfiles
+      )
+    ) {
+      return OCPPConstants.OCPP_RESPONSE_REJECTED;
+    }
+    const startDate = new Date();
+    const endDate = new Date(startDate.getTime() + commandPayload.duration * 1000);
+    let compositeSchedule: OCPP16ChargingSchedule;
+    for (const chargingProfile of chargingStation.getConnectorStatus(commandPayload.connectorId)
+      .chargingProfiles) {
+      // FIXME: build the composite schedule including the local power limit, the stack level, the charging rate unit, etc.
+      if (
+        chargingProfile.chargingSchedule?.startSchedule >= startDate &&
+        chargingProfile.chargingSchedule?.startSchedule <= endDate
+      ) {
+        compositeSchedule = chargingProfile.chargingSchedule;
+        break;
+      }
+    }
+    return {
+      status: GenericStatus.Accepted,
+      scheduleStart: compositeSchedule?.startSchedule,
+      connectorId: commandPayload.connectorId,
+      chargingSchedule: compositeSchedule,
+    };
+  }
+
   private handleRequestClearChargingProfile(
     chargingStation: ChargingStation,
     commandPayload: ClearChargingProfileRequest
index 38c22013e959faf1c34bac73e68cf70f438d71e4..68e9f7c66cae6febf94318a5a5523ef5ee4d6e3d 100644 (file)
@@ -24,6 +24,7 @@ import {
   type OCPP16DataTransferResponse,
   type OCPP16DiagnosticsStatusNotificationResponse,
   type OCPP16FirmwareStatusNotificationResponse,
+  type OCPP16GetCompositeScheduleResponse,
   type OCPP16HeartbeatResponse,
   OCPP16IncomingRequestCommand,
   type OCPP16MeterValuesRequest,
@@ -206,6 +207,14 @@ export class OCPP16ResponseService extends OCPPResponseService {
           'constructor'
         ),
       ],
+      [
+        OCPP16IncomingRequestCommand.GET_COMPOSITE_SCHEDULE,
+        OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16GetCompositeScheduleResponse>(
+          '../../../assets/json-schemas/ocpp/1.6/GetCompositeScheduleResponse.json',
+          moduleName,
+          'constructor'
+        ),
+      ],
       [
         OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
         OCPP16ServiceUtils.parseJsonSchemaFile<SetChargingProfileResponse>(
index a7c7d24efbd8638e004052866c86154885e742ae..6255a3c2ea808594c648c565b82218496df211df 100644 (file)
@@ -102,6 +102,7 @@ export {
   OCPP16ChargePointStatus,
   type OCPP16ChargingProfile,
   OCPP16ChargingProfilePurposeType,
+  type OCPP16ChargingSchedule,
   type OCPP16ClearCacheRequest,
   type OCPP16DataTransferRequest,
   type OCPP16DataTransferResponse,
@@ -113,6 +114,8 @@ export {
   OCPP16FirmwareStatus,
   type OCPP16FirmwareStatusNotificationRequest,
   type OCPP16FirmwareStatusNotificationResponse,
+  type OCPP16GetCompositeScheduleRequest,
+  type OCPP16GetCompositeScheduleResponse,
   type OCPP16HeartbeatRequest,
   type OCPP16HeartbeatResponse,
   OCPP16IncomingRequestCommand,
index 0bfdcd2f717c8330bcef080a81b6c4306a8ebbce..67e12f970056a011b1671d4085bb3eec6c7e5164 100644 (file)
@@ -9,10 +9,10 @@ export interface OCPP16ChargingProfile extends JsonObject {
   recurrencyKind?: OCPP16RecurrencyKindType;
   validFrom?: Date;
   validTo?: Date;
-  chargingSchedule: ChargingSchedule;
+  chargingSchedule: OCPP16ChargingSchedule;
 }
 
-interface ChargingSchedule extends JsonObject {
+export interface OCPP16ChargingSchedule extends JsonObject {
   duration?: number;
   startSchedule?: Date;
   chargingRateUnit: OCPP16ChargingRateUnitType;
index bc45189844dcd9fd09e5f29a717d9375bd25d163..b482d1405e150f1e6178ee17126ca89bbd7a8e7a 100644 (file)
@@ -5,6 +5,7 @@ import type {
   OCPP16ChargePointStatus,
   OCPP16ChargingProfile,
   OCPP16ChargingProfilePurposeType,
+  OCPP16ChargingRateUnitType,
   OCPP16DiagnosticsStatus,
   OCPP16StandardParametersKey,
   OCPP16VendorParametersKey,
@@ -30,6 +31,7 @@ export enum OCPP16IncomingRequestCommand {
   UNLOCK_CONNECTOR = 'UnlockConnector',
   GET_CONFIGURATION = 'GetConfiguration',
   CHANGE_CONFIGURATION = 'ChangeConfiguration',
+  GET_COMPOSITE_SCHEDULE = 'GetCompositeSchedule',
   SET_CHARGING_PROFILE = 'SetChargingProfile',
   CLEAR_CHARGING_PROFILE = 'ClearChargingProfile',
   REMOTE_START_TRANSACTION = 'RemoteStartTransaction',
@@ -100,6 +102,12 @@ export interface ResetRequest extends JsonObject {
   type: ResetType;
 }
 
+export interface OCPP16GetCompositeScheduleRequest extends JsonObject {
+  connectorId: number;
+  duration: number;
+  chargingRateUnit?: OCPP16ChargingRateUnitType;
+}
+
 export interface SetChargingProfileRequest extends JsonObject {
   connectorId: number;
   csChargingProfiles: OCPP16ChargingProfile;
index 1724ce18ba87382495ed2d730f125b94b35c0c66..e7814c4f178caec976040c3d6e9372c155cf1729 100644 (file)
@@ -1,6 +1,8 @@
 import type {
   EmptyObject,
+  GenericStatus,
   JsonObject,
+  OCPP16ChargingSchedule,
   OCPPConfigurationKey,
   RegistrationStatusEnumType,
 } from '../../internal';
@@ -49,6 +51,13 @@ export enum OCPP16ChargingProfileStatus {
   NOT_SUPPORTED = 'NotSupported',
 }
 
+export interface OCPP16GetCompositeScheduleResponse extends JsonObject {
+  status: GenericStatus;
+  connectorId?: number;
+  scheduleStart?: Date;
+  chargingSchedule?: OCPP16ChargingSchedule;
+}
+
 export interface SetChargingProfileResponse extends JsonObject {
   status: OCPP16ChargingProfileStatus;
 }