4 type ChargingStationOptions
,
10 type UIServerConfigurationSection
,
12 import { useToast
} from
'vue-toast-notification'
14 import { randomUUID
, validateUUID
} from
'./Utils'
16 interface ResponseHandler
{
17 procedureName
: ProcedureName
18 reject
: (reason
?: unknown
) => void
19 resolve
: (value
: PromiseLike
<ResponsePayload
> | ResponsePayload
) => void
22 export class UIClient
{
23 private static instance
: null | UIClient
= null
24 private responseHandlers
: Map
<
25 `${string}-${string}-${string}-${string}-${string}`,
29 private ws
?: WebSocket
31 private constructor (private uiServerConfiguration
: UIServerConfigurationSection
) {
33 this.responseHandlers
= new Map
<
34 `${string}-${string}-${string}-${string}-${string}`,
39 public static getInstance (uiServerConfiguration
?: UIServerConfigurationSection
): UIClient
{
40 if (UIClient
.instance
=== null) {
41 if (uiServerConfiguration
== null) {
42 throw new Error('Cannot initialize UIClient if no configuration is provided')
44 UIClient
.instance
= new UIClient(uiServerConfiguration
)
46 return UIClient
.instance
49 private openWS (): void {
51 this.uiServerConfiguration
.authentication
?.enabled
=== true &&
52 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
53 this.uiServerConfiguration
.authentication
.type === AuthenticationType
.PROTOCOL_BASIC_AUTH
55 `${this.uiServerConfiguration.protocol}${this.uiServerConfiguration.version}`,
56 `authorization.basic.${btoa(
57 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
58 `${this.uiServerConfiguration.authentication.username}
:${this.uiServerConfiguration.authentication.password}
`
59 ).replace(/={1,2}$/, '')}`,
61 : `${this.uiServerConfiguration.protocol}${this.uiServerConfiguration.version}`
62 this.ws
= new WebSocket(
64 this.uiServerConfiguration.secure === true
65 ? ApplicationProtocol.WSS
66 : ApplicationProtocol.WS
67 }://${this.uiServerConfiguration.host}:${this.uiServerConfiguration.port.toString()}`,
70 this.ws
.onopen
= () => {
72 `WebSocket to UI server '${this.uiServerConfiguration.host}:${this.uiServerConfiguration.port.toString()}' successfully opened`
75 this.ws
.onmessage
= this.responseHandler
.bind(this)
76 this.ws
.onerror
= errorEvent
=> {
78 `Error in WebSocket to UI server '${this.uiServerConfiguration.host}:${this.uiServerConfiguration.port.toString()}'`
81 `Error in WebSocket to UI server '${this.uiServerConfiguration.host}:${this.uiServerConfiguration.port.toString()}'`,
85 this.ws
.onclose
= () => {
86 useToast().info('WebSocket to UI server closed')
90 private responseHandler (messageEvent
: MessageEvent
<string>): void {
91 let response
: ProtocolResponse
93 response
= JSON
.parse(messageEvent
.data
) as ProtocolResponse
95 useToast().error('Invalid response JSON format')
96 console
.error('Invalid response JSON format', error
)
100 if (!Array.isArray(response
)) {
101 useToast().error('Response not an array')
102 console
.error('Response not an array:', response
)
106 const [uuid
, responsePayload
] = response
108 if (!validateUUID(uuid
)) {
109 useToast().error('Response UUID field is invalid')
110 console
.error('Response UUID field is invalid:', response
)
114 if (this.responseHandlers
.has(uuid
)) {
115 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
116 const { procedureName
, reject
, resolve
} = this.responseHandlers
.get(uuid
)!
117 switch (responsePayload
.status) {
118 case ResponseStatus
.SUCCESS
:
119 resolve(responsePayload
)
121 case ResponseStatus
.FAILURE
:
122 reject(responsePayload
)
127 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
128 `Response status for procedure '${procedureName}' not supported: '${responsePayload.status}'`
132 this.responseHandlers
.delete(uuid
)
134 throw new Error(`Not a response to a request: ${JSON.stringify(response, undefined, 2)}`)
138 private async sendRequest (
139 procedureName
: ProcedureName
,
140 payload
: RequestPayload
141 ): Promise
<ResponsePayload
> {
142 return new Promise
<ResponsePayload
>((resolve
, reject
) => {
143 if (this.ws
?.readyState
=== WebSocket
.OPEN
) {
144 const uuid
= randomUUID()
145 const msg
= JSON
.stringify([uuid
, procedureName
, payload
])
146 const sendTimeout
= setTimeout(() => {
147 this.responseHandlers
.delete(uuid
)
148 reject(new Error(`Send request '${procedureName}' message: connection timeout`))
152 this.responseHandlers
.set(uuid
, { procedureName
, reject
, resolve
})
154 this.responseHandlers
.delete(uuid
)
157 `Send request '${procedureName}' message: error ${(error as Error).toString()}`
161 clearTimeout(sendTimeout
)
164 reject(new Error(`Send request '${procedureName}' message: connection closed`))
169 public async addChargingStations (
171 numberOfStations
: number,
172 options
?: ChargingStationOptions
173 ): Promise
<ResponsePayload
> {
174 return this.sendRequest(ProcedureName
.ADD_CHARGING_STATIONS
, {
181 public async closeConnection (hashId
: string): Promise
<ResponsePayload
> {
182 return this.sendRequest(ProcedureName
.CLOSE_CONNECTION
, {
187 public async deleteChargingStation (hashId
: string): Promise
<ResponsePayload
> {
188 return this.sendRequest(ProcedureName
.DELETE_CHARGING_STATIONS
, {
193 public async listChargingStations (): Promise
<ResponsePayload
> {
194 return this.sendRequest(ProcedureName
.LIST_CHARGING_STATIONS
, {})
197 public async listTemplates (): Promise
<ResponsePayload
> {
198 return this.sendRequest(ProcedureName
.LIST_TEMPLATES
, {})
201 public async openConnection (hashId
: string): Promise
<ResponsePayload
> {
202 return this.sendRequest(ProcedureName
.OPEN_CONNECTION
, {
207 public registerWSEventListener
<K
extends keyof WebSocketEventMap
>(
209 listener
: (event
: WebSocketEventMap
[K
]) => void,
210 options
?: AddEventListenerOptions
| boolean
212 this.ws
?.addEventListener(event
, listener
, options
)
215 public setConfiguration (uiServerConfiguration
: UIServerConfigurationSection
): void {
216 if (this.ws
?.readyState
=== WebSocket
.OPEN
) {
220 this.uiServerConfiguration
= uiServerConfiguration
224 public async setSupervisionUrl (hashId
: string, supervisionUrl
: string): Promise
<ResponsePayload
> {
225 return this.sendRequest(ProcedureName
.SET_SUPERVISION_URL
, {
231 public async simulatorState (): Promise
<ResponsePayload
> {
232 return this.sendRequest(ProcedureName
.SIMULATOR_STATE
, {})
235 public async startAutomaticTransactionGenerator (
238 ): Promise
<ResponsePayload
> {
239 return this.sendRequest(ProcedureName
.START_AUTOMATIC_TRANSACTION_GENERATOR
, {
240 connectorIds
: [connectorId
],
245 public async startChargingStation (hashId
: string): Promise
<ResponsePayload
> {
246 return this.sendRequest(ProcedureName
.START_CHARGING_STATION
, {
251 public async startSimulator (): Promise
<ResponsePayload
> {
252 return this.sendRequest(ProcedureName
.START_SIMULATOR
, {})
255 public async startTransaction (
258 idTag
: string | undefined
259 ): Promise
<ResponsePayload
> {
260 return this.sendRequest(ProcedureName
.START_TRANSACTION
, {
267 public async stopAutomaticTransactionGenerator (
270 ): Promise
<ResponsePayload
> {
271 return this.sendRequest(ProcedureName
.STOP_AUTOMATIC_TRANSACTION_GENERATOR
, {
272 connectorIds
: [connectorId
],
277 public async stopChargingStation (hashId
: string): Promise
<ResponsePayload
> {
278 return this.sendRequest(ProcedureName
.STOP_CHARGING_STATION
, {
283 public async stopSimulator (): Promise
<ResponsePayload
> {
284 return this.sendRequest(ProcedureName
.STOP_SIMULATOR
, {})
287 public async stopTransaction (
289 transactionId
: number | undefined
290 ): Promise
<ResponsePayload
> {
291 return this.sendRequest(ProcedureName
.STOP_TRANSACTION
, {
297 public unregisterWSEventListener
<K
extends keyof WebSocketEventMap
>(
299 listener
: (event
: WebSocketEventMap
[K
]) => void,
300 options
?: AddEventListenerOptions
| boolean
302 this.ws
?.removeEventListener(event
, listener
, options
)