feat: add command to list templates on UI services
[e-mobility-charging-stations-simulator.git] / src / charging-station / ui-server / ui-services / AbstractUIService.ts
1 import { BaseError, type OCPPError } from '../../../exception/index.js'
2 import {
3 BroadcastChannelProcedureName,
4 type BroadcastChannelRequestPayload,
5 type JsonType,
6 ProcedureName,
7 type ProtocolRequest,
8 type ProtocolRequestHandler,
9 type ProtocolResponse,
10 type ProtocolVersion,
11 type RequestPayload,
12 type ResponsePayload,
13 ResponseStatus
14 } from '../../../types/index.js'
15 import { isAsyncFunction, isNotEmptyArray, logger } from '../../../utils/index.js'
16 import { Bootstrap } from '../../Bootstrap.js'
17 import { UIServiceWorkerBroadcastChannel } from '../../broadcast-channel/UIServiceWorkerBroadcastChannel.js'
18 import type { AbstractUIServer } from '../AbstractUIServer.js'
19
20 const moduleName = 'AbstractUIService'
21
22 export abstract class AbstractUIService {
23 protected static readonly ProcedureNameToBroadCastChannelProcedureNameMapping = new Map<
24 ProcedureName,
25 BroadcastChannelProcedureName
26 >([
27 [ProcedureName.START_CHARGING_STATION, BroadcastChannelProcedureName.START_CHARGING_STATION],
28 [ProcedureName.STOP_CHARGING_STATION, BroadcastChannelProcedureName.STOP_CHARGING_STATION],
29 [ProcedureName.CLOSE_CONNECTION, BroadcastChannelProcedureName.CLOSE_CONNECTION],
30 [ProcedureName.OPEN_CONNECTION, BroadcastChannelProcedureName.OPEN_CONNECTION],
31 [
32 ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
33 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR
34 ],
35 [
36 ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
37 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR
38 ],
39 [ProcedureName.SET_SUPERVISION_URL, BroadcastChannelProcedureName.SET_SUPERVISION_URL],
40 [ProcedureName.START_TRANSACTION, BroadcastChannelProcedureName.START_TRANSACTION],
41 [ProcedureName.STOP_TRANSACTION, BroadcastChannelProcedureName.STOP_TRANSACTION],
42 [ProcedureName.AUTHORIZE, BroadcastChannelProcedureName.AUTHORIZE],
43 [ProcedureName.BOOT_NOTIFICATION, BroadcastChannelProcedureName.BOOT_NOTIFICATION],
44 [ProcedureName.STATUS_NOTIFICATION, BroadcastChannelProcedureName.STATUS_NOTIFICATION],
45 [ProcedureName.HEARTBEAT, BroadcastChannelProcedureName.HEARTBEAT],
46 [ProcedureName.METER_VALUES, BroadcastChannelProcedureName.METER_VALUES],
47 [ProcedureName.DATA_TRANSFER, BroadcastChannelProcedureName.DATA_TRANSFER],
48 [
49 ProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
50 BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION
51 ],
52 [
53 ProcedureName.FIRMWARE_STATUS_NOTIFICATION,
54 BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION
55 ]
56 ])
57
58 protected readonly requestHandlers: Map<ProcedureName, ProtocolRequestHandler>
59 private readonly version: ProtocolVersion
60 private readonly uiServer: AbstractUIServer
61 private readonly uiServiceWorkerBroadcastChannel: UIServiceWorkerBroadcastChannel
62 private readonly broadcastChannelRequests: Map<string, number>
63
64 constructor (uiServer: AbstractUIServer, version: ProtocolVersion) {
65 this.uiServer = uiServer
66 this.version = version
67 this.requestHandlers = new Map<ProcedureName, ProtocolRequestHandler>([
68 [ProcedureName.LIST_TEMPLATES, this.handleListTemplates.bind(this)],
69 [ProcedureName.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)],
70 [ProcedureName.START_SIMULATOR, this.handleStartSimulator.bind(this)],
71 [ProcedureName.STOP_SIMULATOR, this.handleStopSimulator.bind(this)]
72 ])
73 this.uiServiceWorkerBroadcastChannel = new UIServiceWorkerBroadcastChannel(this)
74 this.broadcastChannelRequests = new Map<string, number>()
75 }
76
77 public async requestHandler (request: ProtocolRequest): Promise<ProtocolResponse | undefined> {
78 let messageId: string | undefined
79 let command: ProcedureName | undefined
80 let requestPayload: RequestPayload | undefined
81 let responsePayload: ResponsePayload | undefined
82 try {
83 [messageId, command, requestPayload] = request
84
85 if (!this.requestHandlers.has(command)) {
86 throw new BaseError(
87 `${command} is not implemented to handle message payload ${JSON.stringify(
88 requestPayload,
89 undefined,
90 2
91 )}`
92 )
93 }
94
95 // Call the request handler to build the response payload
96 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
97 const requestHandler = this.requestHandlers.get(command)!
98 if (isAsyncFunction(requestHandler)) {
99 responsePayload = await requestHandler(messageId, command, requestPayload)
100 } else {
101 responsePayload = (
102 requestHandler as (
103 uuid?: string,
104 procedureName?: ProcedureName,
105 payload?: RequestPayload
106 ) => undefined | ResponsePayload
107 )(messageId, command, requestPayload)
108 }
109 } catch (error) {
110 // Log
111 logger.error(`${this.logPrefix(moduleName, 'requestHandler')} Handle request error:`, error)
112 responsePayload = {
113 hashIds: requestPayload?.hashIds,
114 status: ResponseStatus.FAILURE,
115 command,
116 requestPayload,
117 responsePayload,
118 errorMessage: (error as OCPPError).message,
119 errorStack: (error as OCPPError).stack,
120 errorDetails: (error as OCPPError).details
121 }
122 }
123 if (responsePayload != null) {
124 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
125 return this.uiServer.buildProtocolResponse(messageId!, responsePayload)
126 }
127 }
128
129 // public sendRequest (
130 // messageId: string,
131 // procedureName: ProcedureName,
132 // requestPayload: RequestPayload
133 // ): void {
134 // this.uiServer.sendRequest(
135 // this.uiServer.buildProtocolRequest(messageId, procedureName, requestPayload)
136 // )
137 // }
138
139 public sendResponse (messageId: string, responsePayload: ResponsePayload): void {
140 if (this.uiServer.hasResponseHandler(messageId)) {
141 this.uiServer.sendResponse(this.uiServer.buildProtocolResponse(messageId, responsePayload))
142 }
143 }
144
145 public logPrefix = (modName: string, methodName: string): string => {
146 return this.uiServer.logPrefix(modName, methodName, this.version)
147 }
148
149 public deleteBroadcastChannelRequest (uuid: string): void {
150 this.broadcastChannelRequests.delete(uuid)
151 }
152
153 public getBroadcastChannelExpectedResponses (uuid: string): number {
154 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
155 return this.broadcastChannelRequests.get(uuid)!
156 }
157
158 protected handleProtocolRequest (
159 uuid: string,
160 procedureName: ProcedureName,
161 payload: RequestPayload
162 ): void {
163 this.sendBroadcastChannelRequest(
164 uuid,
165 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
166 AbstractUIService.ProcedureNameToBroadCastChannelProcedureNameMapping.get(procedureName)!,
167 payload
168 )
169 }
170
171 private sendBroadcastChannelRequest (
172 uuid: string,
173 procedureName: BroadcastChannelProcedureName,
174 payload: BroadcastChannelRequestPayload
175 ): void {
176 if (isNotEmptyArray(payload.hashIds)) {
177 payload.hashIds = payload.hashIds
178 .map(hashId => {
179 if (this.uiServer.chargingStations.has(hashId)) {
180 return hashId
181 }
182 logger.warn(
183 `${this.logPrefix(
184 moduleName,
185 'sendBroadcastChannelRequest'
186 )} Charging station with hashId '${hashId}' not found`
187 )
188 return undefined
189 })
190 .filter(hashId => hashId != null) as string[]
191 } else {
192 delete payload.hashIds
193 }
194 const expectedNumberOfResponses = Array.isArray(payload.hashIds)
195 ? payload.hashIds.length
196 : this.uiServer.chargingStations.size
197 if (expectedNumberOfResponses === 0) {
198 throw new BaseError(
199 'hashIds array in the request payload does not contain any valid charging station hashId'
200 )
201 }
202 this.uiServiceWorkerBroadcastChannel.sendRequest([uuid, procedureName, payload])
203 this.broadcastChannelRequests.set(uuid, expectedNumberOfResponses)
204 }
205
206 private handleListTemplates (): ResponsePayload {
207 return {
208 status: ResponseStatus.SUCCESS,
209 templates: [...this.uiServer.chargingStationTemplates.values()] as JsonType[]
210 } satisfies ResponsePayload
211 }
212
213 private handleListChargingStations (): ResponsePayload {
214 return {
215 status: ResponseStatus.SUCCESS,
216 chargingStations: [...this.uiServer.chargingStations.values()] as JsonType[]
217 } satisfies ResponsePayload
218 }
219
220 private async handleStartSimulator (): Promise<ResponsePayload> {
221 try {
222 await Bootstrap.getInstance().start()
223 return { status: ResponseStatus.SUCCESS }
224 } catch {
225 return { status: ResponseStatus.FAILURE }
226 }
227 }
228
229 private async handleStopSimulator (): Promise<ResponsePayload> {
230 try {
231 await Bootstrap.getInstance().stop()
232 return { status: ResponseStatus.SUCCESS }
233 } catch {
234 return { status: ResponseStatus.FAILURE }
235 }
236 }
237 }