refactor(simulator): cleanup JSON type usage in OCPP payload validation
[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 { OCPP20ServiceUtils } from './OCPP20ServiceUtils';
6 import { type ChargingStation, addConfigurationKey } from '../../../charging-station';
7 import { OCPPError } from '../../../exception';
8 import {
9 ErrorType,
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 { OCPPResponseService } from '../OCPPResponseService';
24
25 const moduleName = 'OCPP20ResponseService';
26
27 export class OCPP20ResponseService extends OCPPResponseService {
28 public jsonIncomingRequestResponseSchemas: Map<
29 OCPP20IncomingRequestCommand,
30 JSONSchemaType<JsonType>
31 >;
32
33 private responseHandlers: Map<OCPP20RequestCommand, ResponseHandler>;
34 private jsonSchemas: Map<OCPP20RequestCommand, JSONSchemaType<JsonType>>;
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 [
43 OCPP20RequestCommand.BOOT_NOTIFICATION,
44 this.handleResponseBootNotification.bind(this) as ResponseHandler,
45 ],
46 [OCPP20RequestCommand.HEARTBEAT, this.emptyResponseHandler.bind(this) as ResponseHandler],
47 [
48 OCPP20RequestCommand.STATUS_NOTIFICATION,
49 this.emptyResponseHandler.bind(this) as ResponseHandler,
50 ],
51 ]);
52 this.jsonSchemas = new Map<OCPP20RequestCommand, JSONSchemaType<JsonType>>([
53 [
54 OCPP20RequestCommand.BOOT_NOTIFICATION,
55 OCPP20ServiceUtils.parseJsonSchemaFile<OCPP20BootNotificationResponse>(
56 'assets/json-schemas/ocpp/2.0/BootNotificationResponse.json',
57 moduleName,
58 'constructor',
59 ),
60 ],
61 [
62 OCPP20RequestCommand.HEARTBEAT,
63 OCPP20ServiceUtils.parseJsonSchemaFile<OCPP20HeartbeatResponse>(
64 'assets/json-schemas/ocpp/2.0/HeartbeatResponse.json',
65 moduleName,
66 'constructor',
67 ),
68 ],
69 [
70 OCPP20RequestCommand.STATUS_NOTIFICATION,
71 OCPP20ServiceUtils.parseJsonSchemaFile<OCPP20StatusNotificationResponse>(
72 'assets/json-schemas/ocpp/2.0/StatusNotificationResponse.json',
73 moduleName,
74 'constructor',
75 ),
76 ],
77 ]);
78 this.jsonIncomingRequestResponseSchemas = new Map([
79 [
80 OCPP20IncomingRequestCommand.CLEAR_CACHE,
81 OCPP20ServiceUtils.parseJsonSchemaFile<OCPP20ClearCacheResponse>(
82 'assets/json-schemas/ocpp/2.0/ClearCacheResponse.json',
83 moduleName,
84 'constructor',
85 ),
86 ],
87 ]);
88 this.validatePayload = this.validatePayload.bind(this) as (
89 chargingStation: ChargingStation,
90 commandName: OCPP20RequestCommand,
91 payload: JsonType,
92 ) => boolean;
93 }
94
95 public async responseHandler<ReqType extends JsonType, ResType extends JsonType>(
96 chargingStation: ChargingStation,
97 commandName: OCPP20RequestCommand,
98 payload: ResType,
99 requestPayload: ReqType,
100 ): Promise<void> {
101 if (
102 chargingStation.isRegistered() === true ||
103 commandName === OCPP20RequestCommand.BOOT_NOTIFICATION
104 ) {
105 if (
106 this.responseHandlers.has(commandName) === true &&
107 OCPP20ServiceUtils.isRequestCommandSupported(chargingStation, commandName) === true
108 ) {
109 try {
110 this.validatePayload(chargingStation, commandName, payload);
111 await this.responseHandlers.get(commandName)!(chargingStation, payload, requestPayload);
112 } catch (error) {
113 logger.error(
114 `${chargingStation.logPrefix()} ${moduleName}.responseHandler: Handle response error:`,
115 error,
116 );
117 throw error;
118 }
119 } else {
120 // Throw exception
121 throw new OCPPError(
122 ErrorType.NOT_IMPLEMENTED,
123 `${commandName} is not implemented to handle response PDU ${JSON.stringify(
124 payload,
125 undefined,
126 2,
127 )}`,
128 commandName,
129 payload,
130 );
131 }
132 } else {
133 throw new OCPPError(
134 ErrorType.SECURITY_ERROR,
135 `${commandName} cannot be issued to handle response PDU ${JSON.stringify(
136 payload,
137 undefined,
138 2,
139 )} while the charging station is not registered on the central server.`,
140 commandName,
141 payload,
142 );
143 }
144 }
145
146 private validatePayload(
147 chargingStation: ChargingStation,
148 commandName: OCPP20RequestCommand,
149 payload: JsonType,
150 ): boolean {
151 if (this.jsonSchemas.has(commandName) === true) {
152 return this.validateResponsePayload(
153 chargingStation,
154 commandName,
155 this.jsonSchemas.get(commandName)!,
156 payload,
157 );
158 }
159 logger.warn(
160 `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command '${commandName}' PDU validation`,
161 );
162 return false;
163 }
164
165 private handleResponseBootNotification(
166 chargingStation: ChargingStation,
167 payload: OCPP20BootNotificationResponse,
168 ): void {
169 if (payload.status === RegistrationStatusEnumType.ACCEPTED) {
170 addConfigurationKey(
171 chargingStation,
172 OCPP20OptionalVariableName.HeartbeatInterval,
173 payload.interval.toString(),
174 {},
175 { overwrite: true, save: true },
176 );
177 OCPP20ServiceUtils.startHeartbeatInterval(chargingStation, payload.interval);
178 }
179 if (Object.values(RegistrationStatusEnumType).includes(payload.status)) {
180 const logMsg = `${chargingStation.logPrefix()} Charging station in '${
181 payload.status
182 }' state on the central server`;
183 payload.status === RegistrationStatusEnumType.REJECTED
184 ? logger.warn(logMsg)
185 : logger.info(logMsg);
186 } else {
187 logger.error(
188 `${chargingStation.logPrefix()} Charging station boot notification response received: %j with undefined registration status`,
189 payload,
190 );
191 }
192 }
193 }