diff --git a/src/components/Connector.tsx b/src/components/Connector.tsx index 257965e..561259c 100644 --- a/src/components/Connector.tsx +++ b/src/components/Connector.tsx @@ -1,6 +1,7 @@ import React, {useState, useEffect} from "react"; import {ChargePoint} from "../cp/ChargePoint.ts"; import * as ocpp from "../cp/OcppTypes"; +import { OCPPAvailability } from "../cp/OcppTypes"; interface ConnectorProps { id: number; @@ -13,8 +14,8 @@ const Connector: React.FC = ({id: connector_id, cp,idTag}) => { const [connectorStatus, setConnectorStatus] = useState( ocpp.OCPPStatus.Unavailable ); - const [availability, setAvailability] = useState( - ocpp.OCPPAvailability.Operative + const [availability, setAvailability] = useState( + "Operative" ); const [meterValue, setMeterValue] = useState(0); const [tagId, setIdTag] = useState(idTag); @@ -223,12 +224,12 @@ const ConnectorStatus: React.FC<{ status: string }> = ({status}) => { return {status}; }; -const ConnectorAvailability: React.FC<{ availability: string }> = ({availability,}) => { - const availabilityColor = (a: string) => { +const ConnectorAvailability: React.FC<{ availability: OCPPAvailability }> = ({availability,}) => { + const availabilityColor = (a: OCPPAvailability) => { switch (a) { - case ocpp.OCPPAvailability.Operative: + case "Operative": return "text-green-500"; - case ocpp.OCPPAvailability.Inoperative: + case "Inoperative": return "text-red-500"; default: return "text-black"; diff --git a/src/cp/ChargePoint.ts b/src/cp/ChargePoint.ts index aa7a6c7..7e228ee 100644 --- a/src/cp/ChargePoint.ts +++ b/src/cp/ChargePoint.ts @@ -28,7 +28,7 @@ export class ChargePoint { | null = null; private _availabilityChangeCallbacks: Map< number, - (availability: string) => void + (availability: OCPPAvailability) => void > = new Map(); constructor(id: string, @@ -121,7 +121,7 @@ export class ChargePoint { setAvailabilityChangeCallback( connectorId: number, - callback: (availability: string) => void + callback: (availability: OCPPAvailability) => void ): void { this._availabilityChangeCallbacks.set(connectorId, callback); } @@ -320,23 +320,24 @@ export class ChargePoint { public updateConnectorAvailability( connectorId: number, - newAvailability: string - ): void { + newAvailability: OCPPAvailability + ): boolean { const connector = this.getConnector(connectorId); - if (connector) { - connector.availability! = newAvailability; - if (newAvailability === OCPPAvailability.Inoperative) { - this.updateConnectorStatus(connectorId, OCPPStatus.Unavailable); - } else if (newAvailability === OCPPAvailability.Operative) { - this.updateConnectorStatus(connectorId, OCPPStatus.Available); - } - const callback = this._availabilityChangeCallbacks.get(connectorId); - if (callback) { - callback(newAvailability); - } - } else { + if (!connector) { this._logger.error(`Connector ${connectorId} not found`); + return false; + } + connector.availability = newAvailability; + if (newAvailability === "Inoperative") { + this.updateConnectorStatus(connectorId, OCPPStatus.Unavailable); + } else if (newAvailability === "Operative") { + this.updateConnectorStatus(connectorId, OCPPStatus.Available); + } + const callback = this._availabilityChangeCallbacks.get(connectorId); + if (callback) { + callback(newAvailability); } + return true; } public setTransactionID(connectorId: number, transactionId: number): void { diff --git a/src/cp/Configuration.ts b/src/cp/Configuration.ts new file mode 100644 index 0000000..bb8a616 --- /dev/null +++ b/src/cp/Configuration.ts @@ -0,0 +1,413 @@ +import { ChargePoint } from "./ChargePoint.ts"; +import { OcppFeatureProfile } from "./OcppTypes.ts"; + +export const ConfigurationKeys = { + Core: { + // If this key exists, the Charge Point supports Unknown Offline Authorization. + // If this key reports a value of true, Unknown Offline Authorization is enabled. + AllowOfflineTxForUnknownId: { + name: "AllowOfflineTxForUnknownId", + required: false, + readonly: false, + type: "boolean" + } as BooleanConfigurationKey, + // If this key exists, the Charge Point supports an Authorization Cache. + // If this key reports a value of true, the Authorization Cache is enabled. + AuthorizationCacheEnabled: { + name: "AuthorizationCacheEnabled", + required: false, + readonly: false, + type: "boolean" + } as BooleanConfigurationKey, + // Whether a remote request to start a transaction in the form of a RemoteStartTransaction.req message should be + // authorized beforehand like a local action to start a transaction. + AuthorizeRemoteTxRequests: { + name: "AuthorizeRemoteTxRequests", + required: true, + readonly: true, // Choice is up to Charge Point implementation + type: "boolean" + } as BooleanConfigurationKey, + // Number of times to blink Charge Point lighting when signalling + BlinkRepeat: { + name: "BlinkRepeat", + required: false, + readonly: false, + type: "integer" + } as IntegerConfigurationKey, + // Size (in seconds) of the clock-aligned data interval. This is the size (in seconds) of the set of evenly spaced + // aggregation intervals per day, starting at 00:00:00 (midnight). + // For example, a value of 900 (15 minutes) indicates that every day should be broken into 96 15-minute intervals. + // When clock aligned data is being transmitted, the interval in question is identified by the start time + // and (optional) duration interval value, represented according to the ISO8601 standard. + // All "per-period" data (e.g. energy readings) should be accumulated (for "flow" type measurands such as energy), + // or averaged (for other values) across the entire interval (or partial interval, at the beginning + // or end of a Transaction), and transmitted (if so enabled) at the end of each interval, + // bearing the interval start time timestamp. + // A value of "0" (numeric zero), by convention, is to be interpreted to mean that no clock-aligned data + // should be transmitted. + ClockAlignedDataInterval: { + name: "ClockAlignedDataInterval", + required: true, + readonly: false, + type: "integer" + } as IntegerConfigurationKey, + // Interval (in seconds) *from beginning of status: 'Preparing' until incipient Transaction is automatically canceled, + // due to failure of EV driver to (correctly) insert the charging cable connector(s) into the appropriate socket(s). + // The Charge Point SHALL go back to the original state, probably: 'Available'. + ConnectionTimeOut: { + name: "ConnectionTimeOut", + required: true, + readonly: false, + type: "integer" + } as IntegerConfigurationKey, + // The phase rotation per connector in respect to the connector’s electrical meter (or if absent, the grid connection). + // Possible values per connector are: + // NotApplicable (for Single phase or DC Charge Points) + // Unknown (not (yet) known) + // RST (Standard Reference Phasing) + // RTS (Reversed Reference Phasing) + // SRT (Reversed 240 degree rotation) + // STR (Standard 120 degree rotation) + // TRS (Standard 240 degree rotation) + // TSR (Reversed 120 degree rotation) + // R can be identified as phase 1 (L1), S as phase 2 (L2), T as phase 3 (L3). + // If known, the Charge Point MAY also report the phase rotation between the grid connection and the main energymeter + // by using index number Zero (0). + // Values are reported in CSL, formatted: 0.RST, 1.RST, 2.RTS + ConnectorPhaseRotation: { + name: "ConnectorPhaseRotation", + required: true, + readonly: false, + type: "array" + } as ArrayConfigurationKey, + // Maximum number of items in a ConnectorPhaseRotation Configuration Key. + ConnectorPhaseRotationMaxLength: { + name: "ConnectorPhaseRotationMaxLength", + required: false, + readonly: true, + type: "integer" + } as IntegerConfigurationKey, + // Maximum number of requested configuration keys in a GetConfiguration.req PDU. + GetConfigurationMaxKeys: { + name: "GetConfigurationMaxKeys", + required: true, + readonly: true, + type: "integer" + } as IntegerConfigurationKey, + // Interval (in seconds) of inactivity (no OCPP exchanges) with central system after which the Charge Point + // should send a Heartbeat.req PDU + HeartbeatInterval: { + name: "HeartbeatInterval", + required: true, + readonly: false, + type: "integer" + } as IntegerConfigurationKey, + // Percentage of maximum intensity at which to illuminate Charge Point lighting + LightIntensity: { + name: "LightIntensity", + required: false, + readonly: false, + type: "integer" + } as IntegerConfigurationKey, + // whether the Charge Point, when offline, will start a transaction for locally-authorized identifiers. + LocalAuthorizeOffline: { + name: "LocalAuthorizeOffline", + required: true, + readonly: false, + type: "boolean" + } as BooleanConfigurationKey, + // whether the Charge Point, when online, will start a transaction for locally-authorized identifiers + // without waiting for or requesting an Authorize.conf from the Central System + LocalPreAuthorize: { + name: "LocalPreAuthorize", + required: true, + readonly: false, + type: "boolean" + } as BooleanConfigurationKey, + // Maximum energy (in Wh) delivered when an identifier is invalidated by the Central System after start of a transaction. + MaxEnergyOnInvalidId: { + name: "MaxEnergyOnInvalidId", + required: false, + readonly: false, + type: "integer" + } as IntegerConfigurationKey, + // Clock-aligned measurand(s) to be included in a MeterValues.req PDU, every ClockAlignedDataInterval seconds + MeterValuesAlignedData: { + name: "MeterValuesAlignedData", + required: true, + readonly: true, + type: "array" + } as ArrayConfigurationKey, + // Maximum number of items in a MeterValuesAlignedData Configuration Key. + MeterValuesAlignedDataMaxLength: { + name: "MeterValuesAlignedDataMaxLength", + required: false, + readonly: true, + type: "integer" + } as IntegerConfigurationKey, + // Sampled measurands to be included in a MeterValues.req PDU, every MeterValueSampleInterval seconds. + // Where applicable, the Measurand is combined with the optional phase; for instance: Voltage.L1 + // Default: "Energy.Active.Import.Register" + MeterValuesSampledData: { + name: "MeterValuesSampledData", + required: true, + readonly: false, + type: "array" + } as ArrayConfigurationKey, + // Maximum number of items in a MeterValuesSampledData Configuration Key. + MeterValuesSampledDataMaxLength: { + name: "MeterValuesSampledDataMaxLength", + required: false, + readonly: true, + type: "integer" + } as IntegerConfigurationKey, + // Interval (in seconds) between sampling of metering (or other) data, intended to be transmitted + // by "MeterValues" PDUs. For charging session data (ConnectorId>0), samples are acquired and transmitted + // periodically at this interval from the start of the charging transaction. + // A value of "0" (numeric zero), by convention, is to be interpreted to mean that no sampled data should be transmitted. + MeterValueSampleInterval: { + name: "MeterValueSampleInterval", + required: true, + readonly: false, + type: "integer" + } as IntegerConfigurationKey, + // The minimum duration (in seconds) that a Charge Point or Connector status is stable before + // a StatusNotification.req PDU is sent to the Central System. + MinimumStatusDuration: { + name: "MinimumStatusDuration", + required: false, + readonly: false, + type: "integer" + } as IntegerConfigurationKey, + // The number of physical charging connectors of this Charge Point. + NumberOfConnectors: { + name: "NumberOfConnectors", + required: true, + readonly: true, + type: "integer" + } as IntegerConfigurationKey, + // Number of times to retry an unsuccessful reset of the Charge Point. + ResetRetries: { + name: "ResetRetries", + required: true, + readonly: false, + type: "integer" + } as IntegerConfigurationKey, + // When set to true, the Charge Point SHALL administratively stop the transaction + // when the cable is unplugged from the EV. + StopTransactionOnEVSideDisconnect: { + name: "StopTransactionOnEVSideDisconnect", + required: true, + readonly: false, + type: "boolean" + } as BooleanConfigurationKey, + // whether the Charge Point will stop an ongoing transaction when it receives a non-Accepted authorization status + // in a StartTransaction.conf for this transaction + StopTransactionOnInvalidId: { + name: "StopTransactionOnInvalidId", + required: true, + readonly: false, + type: "boolean" + } as BooleanConfigurationKey, + // Clock-aligned periodic measurand(s) to be included in the TransactionData element of StopTransaction.req + // MeterValues.req PDU for every ClockAlignedDataInterval of the Transaction + StopTxnAlignedData: { + name: "StopTxnAlignedData", + required: true, + readonly: false, + type: "array" + } as ArrayConfigurationKey, + // Maximum number of items in a StopTxnAlignedData Configuration Key. + StopTxnAlignedDataMaxLength: { + name: "StopTxnAlignedDataMaxLength", + required: false, + readonly: true, + type: "integer" + } as IntegerConfigurationKey, + // Sampled measurands to be included in the TransactionData element of StopTransaction.req PDU, every + // MeterValueSampleInterval seconds from the start of the charging session + StopTxnSampledData: { + name: "StopTxnSampledData", + required: true, + readonly: false, + type: "array" + } as ArrayConfigurationKey, + // Maximum number of items in a StopTxnSampledData Configuration Key. + StopTxnSampledDataMaxLength: { + name: "StopTxnSampledDataMaxLength", + required: false, + readonly: true, + type: "integer" + } as IntegerConfigurationKey, + // A list of supported Feature Profiles. + // Possible profile identifiers: Core, FirmwareManagement, LocalAuthListManagement, Reservation, + // SmartCharging and RemoteTrigger. + SupportedFeatureProfiles: { + name: "SupportedFeatureProfiles", + required: true, + readonly: true, + type: "array" + } as ArrayConfigurationKey, + // Maximum number of items in a SupportedFeatureProfiles Configuration Key. + SupportedFeatureProfilesMaxLength: { + name: "SupportedFeatureProfilesMaxLength", + required: false, + readonly: true, + type: "integer" + } as IntegerConfigurationKey, + // How often (in times) the Charge Point should try to submit a transaction-related message when the Central System fails to process it. + TransactionMessageAttempts: { + name: "TransactionMessageAttempts", + required: true, + readonly: false, + type: "integer" + } as IntegerConfigurationKey, + // How long (in seconds) the Charge Point should wait before resubmitting a transaction-related message + // that the Central System failed to process. + TransactionMessageRetryInterval: { + name: "TransactionMessageRetryInterval", + required: true, + readonly: false, + type: "integer" + } as IntegerConfigurationKey, + // When set to true, the Charge Point SHALL unlock the cable on Charge Point side when the cable is unplugged at the EV. + UnlockConnectorOnEVSideDisconnect: { + name: "UnlockConnectorOnEVSideDisconnect", + required: true, + readonly: false, + type: "boolean" + } as BooleanConfigurationKey, + // Only relevant for websocket implementations. 0 disables client side websocket Ping/Pong. In this case there is either no + // ping/pong or the server initiates the ping and client responds with Pong. Positive values are interpreted as number of seconds + // between pings. Negative values are not allowed. ChangeConfiguration is expected to return a REJECTED result. + WebSocketPingInterval: { + name: "WebSocketPingInterval", + required: false, + readonly: false, + type: "integer" + } as IntegerConfigurationKey + }, + Reservation: { + // If this configuration key is present and set to true: Charge Point support reservations on connector 0. + ReserveConnectorZeroSupported: { + name: "ReserveConnectorZeroSupported", + required: false, + readonly: true, + type: "boolean" + } as BooleanConfigurationKey + }, + LocalAuthListManagement: { + // whether the Local Authorization List is enabled + LocalAuthListEnabled: { + name: "LocalAuthListEnabled", + required: true, + readonly: false, + type: "boolean" + } as BooleanConfigurationKey, + // Maximum number of identifications that can be stored in the Local Authorization List + LocalAuthListMaxLength: { + name: "LocalAuthListMaxLength", + required: true, + readonly: true, + type: "integer" + } as IntegerConfigurationKey, + // Maximum number of identifications that can be send in a single SendLocalList.req + SendLocalListMaxLength: { + name: "SendLocalListMaxLength", + required: true, + readonly: true, + type: "integer" + } as IntegerConfigurationKey + }, + SmartCharging: { + // Max StackLevel of a ChargingProfile. The number defined also indicates the max allowed number of installed charging + // schedules per Charging Profile Purposes. + ChargeProfileMaxStackLevel: { + name: "ChargeProfileMaxStackLevel", + required: true, + readonly: true, + type: "integer" + } as IntegerConfigurationKey, + // A list of supported quantities for use in a ChargingSchedule. Allowed values: 'Current' and 'Power' + ChargingScheduleAllowedChargingRateUnit: { + name: "ChargingScheduleAllowedChargingRateUnit", + required: true, + readonly: true, + type: "array" + } as ArrayConfigurationKey, + // Maximum number of periods that may be defined per ChargingSchedule. + ChargingScheduleMaxPeriods: { + name: "ChargingScheduleMaxPeriods", + required: true, + readonly: true, + type: "integer" + } as IntegerConfigurationKey, + // If defined and true, this Charge Point support switching from 3 to 1 phase during a Transaction. + ConnectorSwitch3to1PhaseSupported: { + name: "ConnectorSwitch3to1PhaseSupported", + required: false, + readonly: true, + type: "boolean" + } as BooleanConfigurationKey, + // Maximum number of Charging profiles installed at a time + MaxChargingProfilesInstalled: { + name: "MaxChargingProfilesInstalled", + required: true, + readonly: true, + type: "integer" + } as IntegerConfigurationKey + }, + Custom: { + OcppServer: { + name: "OcppServer", + required: false, + readonly: false, + type: "string" + } as StringConfigurationKey + }, +}; + +export type ConfigurationKeyType = + | "integer" + | "string" + | "boolean" + | "array" + +export type ConfigurationKey = { + name: string; + readonly: boolean; + required: boolean; + type: T; +} +export type IntegerConfigurationKey = ConfigurationKey<"integer"> +export type StringConfigurationKey = ConfigurationKey<"string"> +export type BooleanConfigurationKey = ConfigurationKey<"boolean"> +export type ArrayConfigurationKey = ConfigurationKey<"array"> + +export type ConfigurationValueType = number | string | boolean | string[] +export type ConfigurationValue = { + key: ConfigurationKey; + value: V; +} + +export type IntegerConfigurationValue = ConfigurationValue<"integer", number> +export type StringConfigurationValue = ConfigurationValue<"string", string> +export type BooleanConfigurationValue = ConfigurationValue<"boolean", boolean> +export type ArrayConfigurationValue = ConfigurationValue<"array", string[]> + +export type Configuration = ConfigurationValue[] + +export const defaultConfiguration: (cp: ChargePoint) => Configuration = (cp) => [ + { + key: ConfigurationKeys.Core.SupportedFeatureProfiles, + value: [OcppFeatureProfile.Core], + } as ArrayConfigurationValue, + { + key: ConfigurationKeys.Core.NumberOfConnectors, + value: cp.connectorNumber, + } as IntegerConfigurationValue, + { + key: ConfigurationKeys.Custom.OcppServer, + value: cp.wsUrl, + } as StringConfigurationValue, +]; diff --git a/src/cp/Connector.ts b/src/cp/Connector.ts index b318d05..4bb25fd 100644 --- a/src/cp/Connector.ts +++ b/src/cp/Connector.ts @@ -17,7 +17,7 @@ export class Connector { constructor(id: number) { this._id = id; this._status = OCPPStatus.Available; - this._availability = OCPPAvailability.Operative; + this._availability = "Operative"; this._meterValue = 0; this._transaction = null; @@ -45,7 +45,7 @@ export class Connector { }) } - get availability(): string { + get availability(): OCPPAvailability { return this._availability; } diff --git a/src/cp/OCPPMessageHandler.ts b/src/cp/OCPPMessageHandler.ts index bb4eb8d..0205bba 100644 --- a/src/cp/OCPPMessageHandler.ts +++ b/src/cp/OCPPMessageHandler.ts @@ -1,35 +1,91 @@ -import {OcppMessageRequestPayload, OcppMessageResponsePayload, OCPPWebSocket} from "./OCPPWebSocket"; -import {ChargePoint} from "./ChargePoint"; -import {Transaction} from "./Transaction"; -import {Logger} from "./Logger"; -import {OCPPMessageType, OCPPAction, OCPPStatus, BootNotification, OCPPErrorCode} from "./OcppTypes"; +import { + OcppMessageErrorPayload, + OcppMessagePayload, + OcppMessageRequestPayload, + OcppMessageResponsePayload, + OCPPWebSocket +} from "./OCPPWebSocket"; +import { ChargePoint } from "./ChargePoint"; +import { Transaction } from "./Transaction"; +import { Logger } from "./Logger"; +import { + BootNotification, + OCPPAction, + OcppConfigurationKey, + OCPPErrorCode, + OCPPMessageType, + OCPPStatus +} from "./OcppTypes"; +import { UploadFile } from "./file_upload.ts"; +import { + ArrayConfigurationValue, + BooleanConfigurationValue, + Configuration, + ConfigurationValue, + defaultConfiguration, + IntegerConfigurationValue, + StringConfigurationValue +} from "./Configuration.ts"; import * as request from "@voltbras/ts-ocpp/dist/messages/json/request"; import * as response from "@voltbras/ts-ocpp/dist/messages/json/response"; -import {OcppMessagePayload} from "./OCPPWebSocket"; - -type OcppMessagePayloadCall = +type CoreOcppMessagePayloadCall = + | request.ChangeAvailabilityRequest + | request.ChangeConfigurationRequest + | request.ClearCacheRequest + | request.GetConfigurationRequest | request.RemoteStartTransactionRequest | request.RemoteStopTransactionRequest | request.ResetRequest + | request.UnlockConnectorRequest + +type FirmwareManagementOcppMessagePayloadCall = | request.GetDiagnosticsRequest - | request.TriggerMessageRequest; + | request.UpdateFirmwareRequest +type LocalAuthListManagementOcppMessagePayloadCall = + | request.GetLocalListVersionRequest + | request.SendLocalListRequest +type ReservationOcppMessagePayloadCall = + | request.CancelReservationRequest + | request.ReserveNowRequest -type OcppMessagePayloadCallResult = +type SmartChargingOcppMessagePayloadCall = + | request.ClearChargingProfileRequest + | request.GetCompositeScheduleRequest + | request.SetChargingProfileRequest + +type RemoteTriggerOcppMessagePayloadCall = + | request.TriggerMessageRequest + +type OcppMessagePayloadCall = + | CoreOcppMessagePayloadCall + | FirmwareManagementOcppMessagePayloadCall + | LocalAuthListManagementOcppMessagePayloadCall + | ReservationOcppMessagePayloadCall + | SmartChargingOcppMessagePayloadCall + | RemoteTriggerOcppMessagePayloadCall + +type CoreOcppMessagePayloadCallResult = | response.AuthorizeResponse | response.BootNotificationResponse + | response.ChangeConfigurationResponse + | response.DataTransferResponse | response.HeartbeatResponse | response.MeterValuesResponse | response.StartTransactionResponse | response.StatusNotificationResponse - | response.StopTransactionResponse; - + | response.StopTransactionResponse +type FirmwareManagementOcppMessagePayloadCallResult = + | response.DiagnosticsStatusNotificationResponse + | response.FirmwareStatusNotificationResponse -import {UploadFile} from "./file_upload.ts"; +type OcppMessagePayloadCallResult = + | CoreOcppMessagePayloadCallResult + | FirmwareManagementOcppMessagePayloadCallResult interface OCPPRequest { type: OCPPMessageType; @@ -211,7 +267,7 @@ export class OCPPMessageHandler { this.handleCallResult(messageId, payload as OcppMessagePayloadCallResult); break; case OCPPMessageType.CALL_ERROR: - this.handleCallError(messageId, payload); + this.handleCallError(messageId, payload as OcppMessageErrorPayload); break; default: this._logger.error(`Unknown message type: ${messageType}`); @@ -244,6 +300,18 @@ export class OCPPMessageHandler { case OCPPAction.TriggerMessage: response = this.handleTriggerMessage(payload as request.TriggerMessageRequest); break; + case OCPPAction.GetConfiguration: + response = this.handleGetConfiguration(payload as request.GetConfigurationRequest); + break; + case OCPPAction.ChangeConfiguration: + response = this.handleChangeConfiguration(payload as request.ChangeConfigurationRequest); + break; + case OCPPAction.ClearCache: + response = this.handleClearCache(payload as request.ClearCacheRequest); + break; + case OCPPAction.UnlockConnector: + response = this.handleUnlockConnector(payload as request.UnlockConnectorRequest); + break; default: this._logger.error(`Unsupported action: ${action}`); this.sendCallError( @@ -267,6 +335,11 @@ export class OCPPMessageHandler { const request = this._requests.get(messageId); const action = request?.action; switch (action) { + case OCPPAction.ChangeAvailability: + this.handleChangeAvailability( + payload as request.ChangeAvailabilityRequest + ); + break; case OCPPAction.BootNotification: this.handleBootNotificationResponse( payload as response.BootNotificationResponse @@ -298,6 +371,9 @@ export class OCPPMessageHandler { payload as response.StatusNotificationResponse ); break; + case OCPPAction.DataTransfer: + this.handleDataTransferResponse(payload as response.DataTransferResponse); + break; default: this._logger.log(`Unsupported action result: ${action}`); } @@ -305,7 +381,7 @@ export class OCPPMessageHandler { this._requests.remove(messageId); } - private handleCallError(messageId: string, error: OcppMessagePayload): void { + private handleCallError(messageId: string, error: OcppMessageErrorPayload): void { this._logger.log( `Received error for message ${messageId}: ${JSON.stringify(error)}` ); @@ -371,6 +447,58 @@ export class OCPPMessageHandler { return {fileName: "diagnostics.txt"}; } + private handleGetConfiguration( + payload: request.GetConfigurationRequest + ): response.GetConfigurationResponse { + this._logger.log(`Get configuration request received: ${JSON.stringify(payload.key)}`); + const configuration = OCPPMessageHandler.mapConfiguration(defaultConfiguration(this._chargePoint)); + if (!payload.key || payload.key.length === 0) { + return { + configurationKey: configuration, + }; + } + const filteredConfig = configuration.filter((c) => payload.key?.includes(c.key)); + const configurationKeys = configuration.map((c) => c.key); + const unknownKeys = payload.key.filter((c) => !configurationKeys.includes(c)); + return { + configurationKey: filteredConfig, + unknownKey: unknownKeys, + } + } + + private static mapConfiguration(config: Configuration): OcppConfigurationKey[] { + return config.map(c => ({ + key: c.key.name, + readonly: c.key.readonly, + value: OCPPMessageHandler.mapValue(c), + })); + } + + private static mapValue(value: ConfigurationValue): string { + switch (value.key.type) { + case "string": + return (value as StringConfigurationValue).value; + case "boolean": + return String((value as BooleanConfigurationValue).value); + case "integer": + return String((value as IntegerConfigurationValue).value); + case "array": + return (value as ArrayConfigurationValue).value.join(','); + } + } + + private handleChangeConfiguration( + payload: request.ChangeConfigurationRequest + ): response.ChangeConfigurationResponse { + this._logger.log(`Change configuration request received: ${JSON.stringify(payload.key)}: ${JSON.stringify(payload.value)}`); + switch (payload.key) { + default: + return { + status: "NotSupported", + }; + } + } + private handleTriggerMessage( payload: request.TriggerMessageRequest ): response.TriggerMessageResponse { @@ -380,6 +508,30 @@ export class OCPPMessageHandler { return {status: "Accepted"}; } + private handleChangeAvailability(payload: request.ChangeAvailabilityRequest): response.ChangeAvailabilityResponse { + this._logger.log(`Change availability request received: ${JSON.stringify(payload)}`); + const updated = this._chargePoint.updateConnectorAvailability(payload.connectorId, payload.type); + if (updated) { + return {status: "Accepted"}; + } else { + return {status: "Rejected"}; + } + } + + private handleClearCache( + payload: request.ClearCacheRequest + ): response.ClearCacheResponse { + this._logger.log(`Clear cache request received: ${JSON.stringify(payload)}`); + return {status: "Accepted"}; + } + + private handleUnlockConnector( + payload: request.UnlockConnectorRequest + ): response.UnlockConnectorResponse { + this._logger.log(`Unlock connector request received: ${JSON.stringify(payload)}`); + return {status: "NotSupported"}; + } + private handleBootNotificationResponse( payload: response.BootNotificationResponse ): void { @@ -463,6 +615,12 @@ export class OCPPMessageHandler { this._logger.log(`Status notification sent successfully: ${JSON.stringify(payload)}`); } + private handleDataTransferResponse( + payload: response.DataTransferResponse + ): void { + this._logger.log(`Data transfer sent successfully: ${JSON.stringify(payload)}`); + } + private sendCallResult(messageId: string, payload: OcppMessageResponsePayload): void { this._webSocket.sendResult( messageId, diff --git a/src/cp/OCPPWebSocket.ts b/src/cp/OCPPWebSocket.ts index da32df7..dabe067 100644 --- a/src/cp/OCPPWebSocket.ts +++ b/src/cp/OCPPWebSocket.ts @@ -12,14 +12,17 @@ export type OcppMessageRequestPayload = | request.MeterValuesRequest | request.StartTransactionRequest | request.StatusNotificationRequest - | request.StopTransactionRequest; + | request.StopTransactionRequest export type OcppMessageResponsePayload = + | response.ChangeConfigurationResponse + | response.GetConfigurationResponse | response.GetDiagnosticsResponse | response.RemoteStartTransactionResponse | response.RemoteStopTransactionResponse | response.ResetResponse - | response.TriggerMessageResponse; + | response.TriggerMessageResponse + | response.UnlockConnectorResponse export type OcppMessageErrorPayload = { readonly errorCode: OCPPErrorCode; diff --git a/src/cp/OcppTypes.ts b/src/cp/OcppTypes.ts index adb584c..5c47b5e 100644 --- a/src/cp/OcppTypes.ts +++ b/src/cp/OcppTypes.ts @@ -12,10 +12,7 @@ export enum OCPPStatus { Faulted = "Faulted", } -export enum OCPPAvailability { - Operative = "Operative", - Inoperative = "Inoperative", -} +export type OCPPAvailability = "Operative" | "Inoperative" export enum OCPPMessageType { CALL = 2, @@ -24,21 +21,64 @@ export enum OCPPMessageType { } export enum OCPPAction { - // Charge Point to Central System - CallResult = "CallResult", - + // Core actions + Authorize = "Authorize", + BootNotification = "BootNotification", + ChangeAvailability = "ChangeAvailability", + ChangeConfiguration = "ChangeConfiguration", + ClearCache = "ClearCache", + DataTransfer = "DataTransfer", + GetConfiguration = "GetConfiguration", + Heartbeat = "Heartbeat", + MeterValues = "MeterValues", RemoteStartTransaction = "RemoteStartTransaction", RemoteStopTransaction = "RemoteStopTransaction", + Reset = "Reset", StartTransaction = "StartTransaction", + StatusNotification = "StatusNotification", StopTransaction = "StopTransaction", + UnlockConnector = "UnlockConnector", + // FirmwareManagement actions GetDiagnostics = "GetDiagnostics", + DiagnosticsStatusNotification = "DiagnosticsStatusNotification", // TODO + FirmwareStatusNotification = "FirmwareStatusNotification", // TODO + UpdateFirmware = "UpdateFirmware", // TODO + // LocalAuthListManagement actions + GetLocalListVersion = "GetLocalListVersion", // TODO + SendLocalList = "SendLocalList", // TODO + // Reservation actions + CancelReservation = "CancelReservation", // TODO + ReserveNow = "ReserveNow", // TODO + // SmartCharging actions + ClearChargingProfile = "ClearChargingProfile", // TODO + GetCompositeSchedule = "GetCompositeSchedule", // TODO + SetChargingProfile = "SetChargingProfile", // TODO + // RemoteTrigger actions TriggerMessage = "TriggerMessage", - StatusNotification = "StatusNotification", - MeterValues = "MeterValues", - BootNotification = "BootNotification", - Heartbeat = "Heartbeat", - Authorize = "Authorize", - Reset = "Reset", + // Fake actions + CallResult = "CallResult", +} + +export enum OcppFeatureProfile { + // Basic Charge Point functionality comparable with OCPP 1.5 [OCPP1.5] + // without support for firmware updates, local authorization list management and reservations. + Core = "Core", + // Support for firmware update management and diagnostic log file download. + FirmwareManagement = "FirmwareManagement", + // Features to manage the local authorization list in Charge Points. + LocalAuthListManagement = "LocalAuthListManagement", + // Support for reservation of a Charge Point. + Reservation = "Reservation", + // Support for basic Smart Charging, for instance using control pilot. + SmartCharging = "SmartCharging", + // Support for remote triggering of Charge Point initiated messages + RemoteTrigger = "RemoteTrigger", +} + +export type OcppConfigurationKey = { + key: string; + readonly: boolean; + value?: string; } export type OCPPErrorCode = ErrorCode;