refactor: add helper to test connector
[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 { 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 = this.validatePayload.bind(this) as (
83 chargingStation: ChargingStation,
84 commandName: OCPP20RequestCommand,
85 payload: JsonType
86 ) => boolean;
87 }
88
89 public async responseHandler(
90 chargingStation: ChargingStation,
91 commandName: OCPP20RequestCommand,
92 payload: JsonType,
93 requestPayload: JsonType
94 ): Promise<void> {
95 if (
96 chargingStation.isRegistered() === true ||
97 commandName === OCPP20RequestCommand.BOOT_NOTIFICATION
98 ) {
99 if (
100 this.responseHandlers.has(commandName) === true &&
101 OCPP20ServiceUtils.isRequestCommandSupported(chargingStation, commandName) === true
102 ) {
103 try {
104 this.validatePayload(chargingStation, commandName, payload);
105 await this.responseHandlers.get(commandName)(chargingStation, payload, requestPayload);
106 } catch (error) {
107 logger.error(
108 `${chargingStation.logPrefix()} ${moduleName}.responseHandler: Handle response error:`,
109 error
110 );
111 throw error;
112 }
113 } else {
114 // Throw exception
115 throw new OCPPError(
116 ErrorType.NOT_IMPLEMENTED,
117 `${commandName} is not implemented to handle response PDU ${JSON.stringify(
118 payload,
119 null,
120 2
121 )}`,
122 commandName,
123 payload
124 );
125 }
126 } else {
127 throw new OCPPError(
128 ErrorType.SECURITY_ERROR,
129 `${commandName} cannot be issued to handle response PDU ${JSON.stringify(
130 payload,
131 null,
132 2
133 )} while the charging station is not registered on the central server.`,
134 commandName,
135 payload
136 );
137 }
138 }
139
140 private validatePayload(
141 chargingStation: ChargingStation,
142 commandName: OCPP20RequestCommand,
143 payload: JsonType
144 ): boolean {
145 if (this.jsonSchemas.has(commandName) === true) {
146 return this.validateResponsePayload(
147 chargingStation,
148 commandName,
149 this.jsonSchemas.get(commandName),
150 payload
151 );
152 }
153 logger.warn(
154 `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command '${commandName}' PDU validation`
155 );
156 return false;
157 }
158
159 private handleResponseBootNotification(
160 chargingStation: ChargingStation,
161 payload: OCPP20BootNotificationResponse
162 ): void {
163 if (payload.status === RegistrationStatusEnumType.ACCEPTED) {
164 ChargingStationConfigurationUtils.addConfigurationKey(
165 chargingStation,
166 OCPP20OptionalVariableName.HeartbeatInterval,
167 payload.interval.toString(),
168 {},
169 { overwrite: true, save: true }
170 );
171 OCPP20ServiceUtils.startHeartbeatInterval(chargingStation, payload.interval);
172 }
173 if (Object.values(RegistrationStatusEnumType).includes(payload.status)) {
174 const logMsg = `${chargingStation.logPrefix()} Charging station in '${
175 payload.status
176 }' state on the central server`;
177 payload.status === RegistrationStatusEnumType.REJECTED
178 ? logger.warn(logMsg)
179 : logger.info(logMsg);
180 } else {
181 logger.error(
182 `${chargingStation.logPrefix()} Charging station boot notification response received: %j with undefined registration status`,
183 payload
184 );
185 }
186 }
187 }