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