129d8e52ddecb00fcefc437abdcedbf2c0eb9650
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 2.0 / OCPP20ResponseService.ts
1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
2
3 import type { JSONSchemaType } from 'ajv';
4
5 import { type ChargingStation, ChargingStationConfigurationUtils } from '../../../charging-station';
6 import { OCPPError } from '../../../exception';
7 import {
8 ErrorType,
9 type JsonObject,
10 type JsonType,
11 type OCPP20BootNotificationResponse,
12 type OCPP20ClearCacheResponse,
13 type OCPP20HeartbeatResponse,
14 OCPP20IncomingRequestCommand,
15 OCPP20OptionalVariableName,
16 OCPP20RequestCommand,
17 type OCPP20StatusNotificationResponse,
18 OCPPVersion,
19 RegistrationStatusEnumType,
20 type ResponseHandler,
21 } from '../../../types';
22 import { Constants, logger } from '../../../utils';
23 import { OCPP20ServiceUtils, OCPPResponseService } from '../internal';
24
25 const moduleName = 'OCPP20ResponseService';
26
27 export class OCPP20ResponseService extends OCPPResponseService {
28 public jsonIncomingRequestResponseSchemas: Map<
29 OCPP20IncomingRequestCommand,
30 JSONSchemaType<JsonObject>
31 >;
32
33 private responseHandlers: Map<OCPP20RequestCommand, ResponseHandler>;
34 private jsonSchemas: Map<OCPP20RequestCommand, JSONSchemaType<JsonObject>>;
35
36 public constructor() {
37 // if (new.target?.name === moduleName) {
38 // throw new TypeError(`Cannot construct ${new.target?.name} instances directly`);
39 // }
40 super(OCPPVersion.VERSION_20);
41 this.responseHandlers = new Map<OCPP20RequestCommand, ResponseHandler>([
42 [OCPP20RequestCommand.BOOT_NOTIFICATION, this.handleResponseBootNotification.bind(this)],
43 [OCPP20RequestCommand.HEARTBEAT, this.emptyResponseHandler.bind(this)],
44 [OCPP20RequestCommand.STATUS_NOTIFICATION, this.emptyResponseHandler.bind(this)],
45 ]);
46 this.jsonSchemas = new Map<OCPP20RequestCommand, JSONSchemaType<JsonObject>>([
47 [
48 OCPP20RequestCommand.BOOT_NOTIFICATION,
49 OCPP20ServiceUtils.parseJsonSchemaFile<OCPP20BootNotificationResponse>(
50 '../../../assets/json-schemas/ocpp/2.0/BootNotificationResponse.json',
51 moduleName,
52 'constructor'
53 ),
54 ],
55 [
56 OCPP20RequestCommand.HEARTBEAT,
57 OCPP20ServiceUtils.parseJsonSchemaFile<OCPP20HeartbeatResponse>(
58 '../../../assets/json-schemas/ocpp/2.0/HeartbeatResponse.json',
59 moduleName,
60 'constructor'
61 ),
62 ],
63 [
64 OCPP20RequestCommand.STATUS_NOTIFICATION,
65 OCPP20ServiceUtils.parseJsonSchemaFile<OCPP20StatusNotificationResponse>(
66 '../../../assets/json-schemas/ocpp/2.0/StatusNotificationResponse.json',
67 moduleName,
68 'constructor'
69 ),
70 ],
71 ]);
72 this.jsonIncomingRequestResponseSchemas = new Map([
73 [
74 OCPP20IncomingRequestCommand.CLEAR_CACHE,
75 OCPP20ServiceUtils.parseJsonSchemaFile<OCPP20ClearCacheResponse>(
76 '../../../assets/json-schemas/ocpp/2.0/ClearCacheResponse.json',
77 moduleName,
78 'constructor'
79 ),
80 ],
81 ]);
82 this.validatePayload.bind(this);
83 }
84
85 public async responseHandler(
86 chargingStation: ChargingStation,
87 commandName: OCPP20RequestCommand,
88 payload: JsonType,
89 requestPayload: JsonType
90 ): Promise<void> {
91 if (
92 chargingStation.isRegistered() === true ||
93 commandName === OCPP20RequestCommand.BOOT_NOTIFICATION
94 ) {
95 if (
96 this.responseHandlers.has(commandName) === true &&
97 OCPP20ServiceUtils.isRequestCommandSupported(chargingStation, commandName) === true
98 ) {
99 try {
100 this.validatePayload(chargingStation, commandName, payload);
101 await this.responseHandlers.get(commandName)(chargingStation, payload, requestPayload);
102 } catch (error) {
103 logger.error(
104 `${chargingStation.logPrefix()} ${moduleName}.responseHandler: Handle response error:`,
105 error
106 );
107 throw error;
108 }
109 } else {
110 // Throw exception
111 throw new OCPPError(
112 ErrorType.NOT_IMPLEMENTED,
113 `${commandName} is not implemented to handle response PDU ${JSON.stringify(
114 payload,
115 null,
116 2
117 )}`,
118 commandName,
119 payload
120 );
121 }
122 } else {
123 throw new OCPPError(
124 ErrorType.SECURITY_ERROR,
125 `${commandName} cannot be issued to handle response PDU ${JSON.stringify(
126 payload,
127 null,
128 2
129 )} while the charging station is not registered on the central server.`,
130 commandName,
131 payload
132 );
133 }
134 }
135
136 private validatePayload(
137 chargingStation: ChargingStation,
138 commandName: OCPP20RequestCommand,
139 payload: JsonType
140 ): boolean {
141 if (this.jsonSchemas.has(commandName) === true) {
142 return this.validateResponsePayload(
143 chargingStation,
144 commandName,
145 this.jsonSchemas.get(commandName),
146 payload
147 );
148 }
149 logger.warn(
150 `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command '${commandName}' PDU validation`
151 );
152 return false;
153 }
154
155 private handleResponseBootNotification(
156 chargingStation: ChargingStation,
157 payload: OCPP20BootNotificationResponse
158 ): void {
159 if (payload.status === RegistrationStatusEnumType.ACCEPTED) {
160 ChargingStationConfigurationUtils.addConfigurationKey(
161 chargingStation,
162 OCPP20OptionalVariableName.HeartbeatInterval,
163 payload.interval.toString(),
164 Constants.EMPTY_OBJECT,
165 { overwrite: true, save: true }
166 );
167 chargingStation.heartbeatSetInterval
168 ? chargingStation.restartHeartbeat()
169 : chargingStation.startHeartbeat();
170 }
171 if (Object.values(RegistrationStatusEnumType).includes(payload.status)) {
172 const logMsg = `${chargingStation.logPrefix()} Charging station in '${
173 payload.status
174 }' state on the central server`;
175 payload.status === RegistrationStatusEnumType.REJECTED
176 ? logger.warn(logMsg)
177 : logger.info(logMsg);
178 } else {
179 logger.error(
180 `${chargingStation.logPrefix()} Charging station boot notification response received: %j with undefined registration status`,
181 payload
182 );
183 }
184 }
185 }