refactor: more coding style fixes
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / OCPPServiceUtils.ts
CommitLineData
66a7748d
JB
1import { readFileSync } from 'node:fs'
2import { dirname, join } from 'node:path'
3import { fileURLToPath } from 'node:url'
7164966d 4
66a7748d
JB
5import type { DefinedError, ErrorObject, JSONSchemaType } from 'ajv'
6import { isDate } from 'date-fns'
06ad945f 7
66a7748d
JB
8import { OCPP16Constants } from './1.6/OCPP16Constants.js'
9import { OCPP20Constants } from './2.0/OCPP20Constants.js'
10import { OCPPConstants } from './OCPPConstants.js'
a6ef1ece
JB
11import {
12 type ChargingStation,
13 getConfigurationKey,
66a7748d
JB
14 getIdTagsFile
15} from '../../charging-station/index.js'
16import { BaseError, OCPPError } from '../../exception/index.js'
6e939d9e 17import {
ae725be3
JB
18 AuthorizationStatus,
19 type AuthorizeRequest,
20 type AuthorizeResponse,
268a74bb 21 ChargePointErrorCode,
3e888c65 22 ChargingStationEvents,
ae725be3 23 type ConnectorStatus,
268a74bb 24 type ConnectorStatusEnum,
41f3983a 25 CurrentType,
268a74bb
JB
26 ErrorType,
27 FileType,
6e939d9e 28 IncomingRequestCommand,
268a74bb 29 type JsonType,
41f3983a
JB
30 type MeasurandPerPhaseSampledValueTemplates,
31 type MeasurandValues,
6e939d9e 32 MessageTrigger,
268a74bb 33 MessageType,
41f3983a
JB
34 type MeterValue,
35 MeterValueContext,
36 MeterValueLocation,
268a74bb 37 MeterValueMeasurand,
41f3983a
JB
38 MeterValuePhase,
39 MeterValueUnit,
268a74bb
JB
40 type OCPP16StatusNotificationRequest,
41 type OCPP20StatusNotificationRequest,
42 OCPPVersion,
6e939d9e 43 RequestCommand,
41f3983a 44 type SampledValue,
268a74bb
JB
45 type SampledValueTemplate,
46 StandardParametersKey,
6e939d9e 47 type StatusNotificationRequest,
66a7748d
JB
48 type StatusNotificationResponse
49} from '../../types/index.js'
9bf0ef23 50import {
41f3983a
JB
51 ACElectricUtils,
52 Constants,
53 DCElectricUtils,
54 convertToFloat,
55 convertToInt,
56 getRandomFloatFluctuatedRounded,
57 getRandomFloatRounded,
58 getRandomInteger,
9bf0ef23
JB
59 handleFileException,
60 isNotEmptyArray,
61 isNotEmptyString,
41f3983a
JB
62 isNullOrUndefined,
63 isUndefined,
9bf0ef23
JB
64 logPrefix,
65 logger,
d71ce3fa 66 max,
5adf6ca4 67 min,
66a7748d
JB
68 roundTo
69} from '../../utils/index.js'
06ad945f 70
041365be
JB
71export const getMessageTypeString = (messageType: MessageType): string => {
72 switch (messageType) {
73 case MessageType.CALL_MESSAGE:
66a7748d 74 return 'request'
041365be 75 case MessageType.CALL_RESULT_MESSAGE:
66a7748d 76 return 'response'
041365be 77 case MessageType.CALL_ERROR_MESSAGE:
66a7748d 78 return 'error'
041365be 79 default:
66a7748d 80 return 'unknown'
041365be 81 }
66a7748d 82}
041365be
JB
83
84export const buildStatusNotificationRequest = (
85 chargingStation: ChargingStation,
86 connectorId: number,
87 status: ConnectorStatusEnum,
66a7748d 88 evseId?: number
041365be
JB
89): StatusNotificationRequest => {
90 switch (chargingStation.stationInfo?.ocppVersion) {
91 case OCPPVersion.VERSION_16:
92 return {
93 connectorId,
94 status,
66a7748d
JB
95 errorCode: ChargePointErrorCode.NO_ERROR
96 } satisfies OCPP16StatusNotificationRequest
041365be
JB
97 case OCPPVersion.VERSION_20:
98 case OCPPVersion.VERSION_201:
99 return {
100 timestamp: new Date(),
101 connectorStatus: status,
102 connectorId,
66a7748d
JB
103 evseId
104 } satisfies OCPP20StatusNotificationRequest
041365be 105 default:
66a7748d 106 throw new BaseError('Cannot build status notification payload: OCPP version not supported')
041365be 107 }
66a7748d 108}
041365be
JB
109
110export const isIdTagAuthorized = async (
111 chargingStation: ChargingStation,
112 connectorId: number,
66a7748d 113 idTag: string
041365be
JB
114): Promise<boolean> => {
115 if (
116 !chargingStation.getLocalAuthListEnabled() &&
66a7748d 117 chargingStation.stationInfo?.remoteAuthorization === false
041365be
JB
118 ) {
119 logger.warn(
66a7748d
JB
120 `${chargingStation.logPrefix()} The charging station expects to authorize RFID tags but nor local authorization nor remote authorization are enabled. Misbehavior may occur`
121 )
041365be 122 }
66a7748d
JB
123 if (chargingStation.getLocalAuthListEnabled() && isIdTagLocalAuthorized(chargingStation, idTag)) {
124 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
125 const connectorStatus: ConnectorStatus = chargingStation.getConnectorStatus(connectorId)!
126 connectorStatus.localAuthorizeIdTag = idTag
127 connectorStatus.idTagLocalAuthorized = true
128 return true
129 } else if (chargingStation.stationInfo?.remoteAuthorization === true) {
130 return await isIdTagRemoteAuthorized(chargingStation, connectorId, idTag)
041365be 131 }
66a7748d
JB
132 return false
133}
041365be
JB
134
135const isIdTagLocalAuthorized = (chargingStation: ChargingStation, idTag: string): boolean => {
136 return (
66a7748d 137 chargingStation.hasIdTags() &&
041365be
JB
138 isNotEmptyString(
139 chargingStation.idTagsCache
66a7748d 140 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
041365be 141 .getIdTags(getIdTagsFile(chargingStation.stationInfo)!)
66a7748d 142 ?.find((tag) => tag === idTag)
041365be 143 )
66a7748d
JB
144 )
145}
041365be
JB
146
147const isIdTagRemoteAuthorized = async (
148 chargingStation: ChargingStation,
149 connectorId: number,
66a7748d 150 idTag: string
041365be 151): Promise<boolean> => {
66a7748d
JB
152 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
153 chargingStation.getConnectorStatus(connectorId)!.authorizeIdTag = idTag
041365be
JB
154 return (
155 (
156 await chargingStation.ocppRequestService.requestHandler<AuthorizeRequest, AuthorizeResponse>(
157 chargingStation,
158 RequestCommand.AUTHORIZE,
159 {
66a7748d
JB
160 idTag
161 }
041365be
JB
162 )
163 )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED
66a7748d
JB
164 )
165}
041365be
JB
166
167export const sendAndSetConnectorStatus = async (
168 chargingStation: ChargingStation,
169 connectorId: number,
170 status: ConnectorStatusEnum,
171 evseId?: number,
66a7748d 172 options?: { send: boolean }
041365be 173): Promise<void> => {
66a7748d 174 options = { send: true, ...options }
041365be 175 if (options.send) {
66a7748d 176 checkConnectorStatusTransition(chargingStation, connectorId, status)
041365be 177 await chargingStation.ocppRequestService.requestHandler<
66a7748d
JB
178 StatusNotificationRequest,
179 StatusNotificationResponse
041365be
JB
180 >(
181 chargingStation,
182 RequestCommand.STATUS_NOTIFICATION,
66a7748d
JB
183 buildStatusNotificationRequest(chargingStation, connectorId, status, evseId)
184 )
041365be 185 }
66a7748d
JB
186 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
187 chargingStation.getConnectorStatus(connectorId)!.status = status
041365be
JB
188 chargingStation.emit(ChargingStationEvents.connectorStatusChanged, {
189 connectorId,
66a7748d
JB
190 ...chargingStation.getConnectorStatus(connectorId)
191 })
192}
041365be
JB
193
194const checkConnectorStatusTransition = (
195 chargingStation: ChargingStation,
196 connectorId: number,
66a7748d 197 status: ConnectorStatusEnum
041365be 198): boolean => {
66a7748d
JB
199 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
200 const fromStatus = chargingStation.getConnectorStatus(connectorId)!.status
201 let transitionAllowed = false
041365be
JB
202 switch (chargingStation.stationInfo?.ocppVersion) {
203 case OCPPVersion.VERSION_16:
204 if (
205 (connectorId === 0 &&
206 OCPP16Constants.ChargePointStatusChargingStationTransitions.findIndex(
66a7748d 207 (transition) => transition.from === fromStatus && transition.to === status
041365be
JB
208 ) !== -1) ||
209 (connectorId > 0 &&
210 OCPP16Constants.ChargePointStatusConnectorTransitions.findIndex(
66a7748d 211 (transition) => transition.from === fromStatus && transition.to === status
041365be
JB
212 ) !== -1)
213 ) {
66a7748d 214 transitionAllowed = true
041365be 215 }
66a7748d 216 break
041365be
JB
217 case OCPPVersion.VERSION_20:
218 case OCPPVersion.VERSION_201:
219 if (
220 (connectorId === 0 &&
221 OCPP20Constants.ChargingStationStatusTransitions.findIndex(
66a7748d 222 (transition) => transition.from === fromStatus && transition.to === status
041365be
JB
223 ) !== -1) ||
224 (connectorId > 0 &&
225 OCPP20Constants.ConnectorStatusTransitions.findIndex(
66a7748d 226 (transition) => transition.from === fromStatus && transition.to === status
041365be
JB
227 ) !== -1)
228 ) {
66a7748d 229 transitionAllowed = true
041365be 230 }
66a7748d 231 break
041365be
JB
232 default:
233 throw new BaseError(
66a7748d
JB
234 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`
235 )
041365be 236 }
66a7748d 237 if (!transitionAllowed) {
041365be
JB
238 logger.warn(
239 `${chargingStation.logPrefix()} OCPP ${chargingStation.stationInfo
240 ?.ocppVersion} connector id ${connectorId} status transition from '${
66a7748d 241 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
041365be 242 chargingStation.getConnectorStatus(connectorId)!.status
66a7748d
JB
243 }' to '${status}' is not allowed`
244 )
041365be 245 }
66a7748d
JB
246 return transitionAllowed
247}
041365be 248
41f3983a
JB
249export const buildMeterValue = (
250 chargingStation: ChargingStation,
251 connectorId: number,
252 transactionId: number,
253 interval: number,
66a7748d 254 debug = false
41f3983a 255): MeterValue => {
66a7748d
JB
256 const connector = chargingStation.getConnectorStatus(connectorId)
257 let meterValue: MeterValue
258 let socSampledValueTemplate: SampledValueTemplate | undefined
259 let voltageSampledValueTemplate: SampledValueTemplate | undefined
260 let powerSampledValueTemplate: SampledValueTemplate | undefined
261 let powerPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {}
262 let currentSampledValueTemplate: SampledValueTemplate | undefined
263 let currentPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {}
264 let energySampledValueTemplate: SampledValueTemplate | undefined
41f3983a
JB
265 switch (chargingStation.stationInfo?.ocppVersion) {
266 case OCPPVersion.VERSION_16:
267 meterValue = {
268 timestamp: new Date(),
66a7748d
JB
269 sampledValue: []
270 }
41f3983a
JB
271 // SoC measurand
272 socSampledValueTemplate = getSampledValueTemplate(
273 chargingStation,
274 connectorId,
66a7748d
JB
275 MeterValueMeasurand.STATE_OF_CHARGE
276 )
277 if (socSampledValueTemplate != null) {
278 const socMaximumValue = 100
279 const socMinimumValue = socSampledValueTemplate.minimumValue ?? 0
41f3983a
JB
280 const socSampledValueTemplateValue = isNotEmptyString(socSampledValueTemplate.value)
281 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
282 parseInt(socSampledValueTemplate.value),
283 socSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT
284 )
285 : getRandomInteger(socMaximumValue, socMinimumValue)
41f3983a 286 meterValue.sampledValue.push(
66a7748d
JB
287 buildSampledValue(socSampledValueTemplate, socSampledValueTemplateValue)
288 )
289 const sampledValuesIndex = meterValue.sampledValue.length - 1
41f3983a
JB
290 if (
291 convertToInt(meterValue.sampledValue[sampledValuesIndex].value) > socMaximumValue ||
292 convertToInt(meterValue.sampledValue[sampledValuesIndex].value) < socMinimumValue ||
293 debug
294 ) {
295 logger.error(
296 `${chargingStation.logPrefix()} MeterValues measurand ${
297 meterValue.sampledValue[sampledValuesIndex].measurand ??
298 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
299 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${socMinimumValue}/${
300 meterValue.sampledValue[sampledValuesIndex].value
66a7748d
JB
301 }/${socMaximumValue}`
302 )
41f3983a
JB
303 }
304 }
305 // Voltage measurand
306 voltageSampledValueTemplate = getSampledValueTemplate(
307 chargingStation,
308 connectorId,
66a7748d
JB
309 MeterValueMeasurand.VOLTAGE
310 )
311 if (voltageSampledValueTemplate != null) {
41f3983a
JB
312 const voltageSampledValueTemplateValue = isNotEmptyString(voltageSampledValueTemplate.value)
313 ? parseInt(voltageSampledValueTemplate.value)
66a7748d
JB
314 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
315 chargingStation.stationInfo.voltageOut!
41f3983a 316 const fluctuationPercent =
66a7748d 317 voltageSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT
41f3983a
JB
318 const voltageMeasurandValue = getRandomFloatFluctuatedRounded(
319 voltageSampledValueTemplateValue,
66a7748d
JB
320 fluctuationPercent
321 )
41f3983a
JB
322 if (
323 chargingStation.getNumberOfPhases() !== 3 ||
324 (chargingStation.getNumberOfPhases() === 3 &&
66a7748d 325 chargingStation.stationInfo?.mainVoltageMeterValues === true)
41f3983a
JB
326 ) {
327 meterValue.sampledValue.push(
66a7748d
JB
328 buildSampledValue(voltageSampledValueTemplate, voltageMeasurandValue)
329 )
41f3983a
JB
330 }
331 for (
332 let phase = 1;
333 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
334 phase++
335 ) {
66a7748d 336 const phaseLineToNeutralValue = `L${phase}-N`
41f3983a
JB
337 const voltagePhaseLineToNeutralSampledValueTemplate = getSampledValueTemplate(
338 chargingStation,
339 connectorId,
340 MeterValueMeasurand.VOLTAGE,
66a7748d
JB
341 phaseLineToNeutralValue as MeterValuePhase
342 )
343 let voltagePhaseLineToNeutralMeasurandValue: number | undefined
344 if (voltagePhaseLineToNeutralSampledValueTemplate != null) {
41f3983a 345 const voltagePhaseLineToNeutralSampledValueTemplateValue = isNotEmptyString(
66a7748d 346 voltagePhaseLineToNeutralSampledValueTemplate.value
41f3983a
JB
347 )
348 ? parseInt(voltagePhaseLineToNeutralSampledValueTemplate.value)
66a7748d
JB
349 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
350 chargingStation.stationInfo.voltageOut!
41f3983a
JB
351 const fluctuationPhaseToNeutralPercent =
352 voltagePhaseLineToNeutralSampledValueTemplate.fluctuationPercent ??
66a7748d 353 Constants.DEFAULT_FLUCTUATION_PERCENT
41f3983a
JB
354 voltagePhaseLineToNeutralMeasurandValue = getRandomFloatFluctuatedRounded(
355 voltagePhaseLineToNeutralSampledValueTemplateValue,
66a7748d
JB
356 fluctuationPhaseToNeutralPercent
357 )
41f3983a
JB
358 }
359 meterValue.sampledValue.push(
360 buildSampledValue(
361 voltagePhaseLineToNeutralSampledValueTemplate ?? voltageSampledValueTemplate,
362 voltagePhaseLineToNeutralMeasurandValue ?? voltageMeasurandValue,
363 undefined,
66a7748d
JB
364 phaseLineToNeutralValue as MeterValuePhase
365 )
366 )
367 if (chargingStation.stationInfo?.phaseLineToLineVoltageMeterValues === true) {
41f3983a
JB
368 const phaseLineToLineValue = `L${phase}-L${
369 (phase + 1) % chargingStation.getNumberOfPhases() !== 0
370 ? (phase + 1) % chargingStation.getNumberOfPhases()
371 : chargingStation.getNumberOfPhases()
66a7748d 372 }`
41f3983a
JB
373 const voltagePhaseLineToLineValueRounded = roundTo(
374 Math.sqrt(chargingStation.getNumberOfPhases()) *
66a7748d 375 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
41f3983a 376 chargingStation.stationInfo.voltageOut!,
66a7748d
JB
377 2
378 )
41f3983a
JB
379 const voltagePhaseLineToLineSampledValueTemplate = getSampledValueTemplate(
380 chargingStation,
381 connectorId,
382 MeterValueMeasurand.VOLTAGE,
66a7748d
JB
383 phaseLineToLineValue as MeterValuePhase
384 )
385 let voltagePhaseLineToLineMeasurandValue: number | undefined
386 if (voltagePhaseLineToLineSampledValueTemplate != null) {
41f3983a 387 const voltagePhaseLineToLineSampledValueTemplateValue = isNotEmptyString(
66a7748d 388 voltagePhaseLineToLineSampledValueTemplate.value
41f3983a
JB
389 )
390 ? parseInt(voltagePhaseLineToLineSampledValueTemplate.value)
66a7748d 391 : voltagePhaseLineToLineValueRounded
41f3983a
JB
392 const fluctuationPhaseLineToLinePercent =
393 voltagePhaseLineToLineSampledValueTemplate.fluctuationPercent ??
66a7748d 394 Constants.DEFAULT_FLUCTUATION_PERCENT
41f3983a
JB
395 voltagePhaseLineToLineMeasurandValue = getRandomFloatFluctuatedRounded(
396 voltagePhaseLineToLineSampledValueTemplateValue,
66a7748d
JB
397 fluctuationPhaseLineToLinePercent
398 )
41f3983a
JB
399 }
400 const defaultVoltagePhaseLineToLineMeasurandValue = getRandomFloatFluctuatedRounded(
401 voltagePhaseLineToLineValueRounded,
66a7748d
JB
402 fluctuationPercent
403 )
41f3983a
JB
404 meterValue.sampledValue.push(
405 buildSampledValue(
406 voltagePhaseLineToLineSampledValueTemplate ?? voltageSampledValueTemplate,
407 voltagePhaseLineToLineMeasurandValue ?? defaultVoltagePhaseLineToLineMeasurandValue,
408 undefined,
66a7748d
JB
409 phaseLineToLineValue as MeterValuePhase
410 )
411 )
41f3983a
JB
412 }
413 }
414 }
415 // Power.Active.Import measurand
416 powerSampledValueTemplate = getSampledValueTemplate(
417 chargingStation,
418 connectorId,
66a7748d
JB
419 MeterValueMeasurand.POWER_ACTIVE_IMPORT
420 )
41f3983a
JB
421 if (chargingStation.getNumberOfPhases() === 3) {
422 powerPerPhaseSampledValueTemplates = {
423 L1: getSampledValueTemplate(
424 chargingStation,
425 connectorId,
426 MeterValueMeasurand.POWER_ACTIVE_IMPORT,
66a7748d 427 MeterValuePhase.L1_N
41f3983a
JB
428 ),
429 L2: getSampledValueTemplate(
430 chargingStation,
431 connectorId,
432 MeterValueMeasurand.POWER_ACTIVE_IMPORT,
66a7748d 433 MeterValuePhase.L2_N
41f3983a
JB
434 ),
435 L3: getSampledValueTemplate(
436 chargingStation,
437 connectorId,
438 MeterValueMeasurand.POWER_ACTIVE_IMPORT,
66a7748d
JB
439 MeterValuePhase.L3_N
440 )
441 }
41f3983a 442 }
66a7748d
JB
443 if (powerSampledValueTemplate != null) {
444 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
445 checkMeasurandPowerDivider(chargingStation, powerSampledValueTemplate.measurand!)
41f3983a
JB
446 const errMsg = `MeterValues measurand ${
447 powerSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
448 }: Unknown ${chargingStation.stationInfo?.currentOutType} currentOutType in template file ${
449 chargingStation.templateFile
450 }, cannot calculate ${
451 powerSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
66a7748d
JB
452 } measurand value`
453 // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
454 const powerMeasurandValues: MeasurandValues = {} as MeasurandValues
455 const unitDivider = powerSampledValueTemplate?.unit === MeterValueUnit.KILO_WATT ? 1000 : 1
41f3983a 456 const connectorMaximumAvailablePower =
66a7748d
JB
457 chargingStation.getConnectorMaximumAvailablePower(connectorId)
458 const connectorMaximumPower = Math.round(connectorMaximumAvailablePower)
41f3983a 459 const connectorMaximumPowerPerPhase = Math.round(
66a7748d
JB
460 connectorMaximumAvailablePower / chargingStation.getNumberOfPhases()
461 )
462 const connectorMinimumPower = Math.round(powerSampledValueTemplate.minimumValue ?? 0)
41f3983a 463 const connectorMinimumPowerPerPhase = Math.round(
66a7748d
JB
464 connectorMinimumPower / chargingStation.getNumberOfPhases()
465 )
41f3983a
JB
466 switch (chargingStation.stationInfo?.currentOutType) {
467 case CurrentType.AC:
468 if (chargingStation.getNumberOfPhases() === 3) {
469 const defaultFluctuatedPowerPerPhase = isNotEmptyString(
66a7748d 470 powerSampledValueTemplate.value
41f3983a
JB
471 )
472 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
473 getLimitFromSampledValueTemplateCustomValue(
474 powerSampledValueTemplate.value,
475 connectorMaximumPower / unitDivider,
476 connectorMinimumPower / unitDivider,
477 {
478 limitationEnabled:
41f3983a 479 chargingStation.stationInfo?.customValueLimitationMeterValues,
66a7748d
JB
480 fallbackValue: connectorMinimumPower / unitDivider
481 }
482 ) / chargingStation.getNumberOfPhases(),
483 powerSampledValueTemplate.fluctuationPercent ??
484 Constants.DEFAULT_FLUCTUATION_PERCENT
485 )
486 : undefined
41f3983a 487 const phase1FluctuatedValue = isNotEmptyString(
66a7748d 488 powerPerPhaseSampledValueTemplates.L1?.value
41f3983a
JB
489 )
490 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
491 getLimitFromSampledValueTemplateCustomValue(
492 powerPerPhaseSampledValueTemplates.L1?.value,
493 connectorMaximumPowerPerPhase / unitDivider,
494 connectorMinimumPowerPerPhase / unitDivider,
495 {
496 limitationEnabled:
41f3983a 497 chargingStation.stationInfo?.customValueLimitationMeterValues,
66a7748d
JB
498 fallbackValue: connectorMinimumPowerPerPhase / unitDivider
499 }
500 ),
501 powerPerPhaseSampledValueTemplates.L1?.fluctuationPercent ??
502 Constants.DEFAULT_FLUCTUATION_PERCENT
503 )
504 : undefined
41f3983a 505 const phase2FluctuatedValue = isNotEmptyString(
66a7748d 506 powerPerPhaseSampledValueTemplates.L2?.value
41f3983a
JB
507 )
508 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
509 getLimitFromSampledValueTemplateCustomValue(
510 powerPerPhaseSampledValueTemplates.L2?.value,
511 connectorMaximumPowerPerPhase / unitDivider,
512 connectorMinimumPowerPerPhase / unitDivider,
513 {
514 limitationEnabled:
41f3983a 515 chargingStation.stationInfo?.customValueLimitationMeterValues,
66a7748d
JB
516 fallbackValue: connectorMinimumPowerPerPhase / unitDivider
517 }
518 ),
519 powerPerPhaseSampledValueTemplates.L2?.fluctuationPercent ??
520 Constants.DEFAULT_FLUCTUATION_PERCENT
521 )
522 : undefined
41f3983a 523 const phase3FluctuatedValue = isNotEmptyString(
66a7748d 524 powerPerPhaseSampledValueTemplates.L3?.value
41f3983a
JB
525 )
526 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
527 getLimitFromSampledValueTemplateCustomValue(
528 powerPerPhaseSampledValueTemplates.L3?.value,
529 connectorMaximumPowerPerPhase / unitDivider,
530 connectorMinimumPowerPerPhase / unitDivider,
531 {
532 limitationEnabled:
41f3983a 533 chargingStation.stationInfo?.customValueLimitationMeterValues,
66a7748d
JB
534 fallbackValue: connectorMinimumPowerPerPhase / unitDivider
535 }
536 ),
537 powerPerPhaseSampledValueTemplates.L3?.fluctuationPercent ??
538 Constants.DEFAULT_FLUCTUATION_PERCENT
539 )
540 : undefined
41f3983a
JB
541 powerMeasurandValues.L1 =
542 phase1FluctuatedValue ??
543 defaultFluctuatedPowerPerPhase ??
544 getRandomFloatRounded(
545 connectorMaximumPowerPerPhase / unitDivider,
66a7748d
JB
546 connectorMinimumPowerPerPhase / unitDivider
547 )
41f3983a
JB
548 powerMeasurandValues.L2 =
549 phase2FluctuatedValue ??
550 defaultFluctuatedPowerPerPhase ??
551 getRandomFloatRounded(
552 connectorMaximumPowerPerPhase / unitDivider,
66a7748d
JB
553 connectorMinimumPowerPerPhase / unitDivider
554 )
41f3983a
JB
555 powerMeasurandValues.L3 =
556 phase3FluctuatedValue ??
557 defaultFluctuatedPowerPerPhase ??
558 getRandomFloatRounded(
559 connectorMaximumPowerPerPhase / unitDivider,
66a7748d
JB
560 connectorMinimumPowerPerPhase / unitDivider
561 )
41f3983a
JB
562 } else {
563 powerMeasurandValues.L1 = isNotEmptyString(powerSampledValueTemplate.value)
564 ? getRandomFloatFluctuatedRounded(
41f3983a
JB
565 getLimitFromSampledValueTemplateCustomValue(
566 powerSampledValueTemplate.value,
567 connectorMaximumPower / unitDivider,
568 connectorMinimumPower / unitDivider,
569 {
570 limitationEnabled:
66a7748d
JB
571 chargingStation.stationInfo?.customValueLimitationMeterValues,
572 fallbackValue: connectorMinimumPower / unitDivider
573 }
41f3983a
JB
574 ),
575 powerSampledValueTemplate.fluctuationPercent ??
66a7748d 576 Constants.DEFAULT_FLUCTUATION_PERCENT
41f3983a 577 )
66a7748d
JB
578 : getRandomFloatRounded(
579 connectorMaximumPower / unitDivider,
580 connectorMinimumPower / unitDivider
581 )
582 powerMeasurandValues.L2 = 0
583 powerMeasurandValues.L3 = 0
584 }
585 powerMeasurandValues.allPhases = roundTo(
586 powerMeasurandValues.L1 + powerMeasurandValues.L2 + powerMeasurandValues.L3,
587 2
588 )
589 break
590 case CurrentType.DC:
591 powerMeasurandValues.allPhases = isNotEmptyString(powerSampledValueTemplate.value)
592 ? getRandomFloatFluctuatedRounded(
593 getLimitFromSampledValueTemplateCustomValue(
594 powerSampledValueTemplate.value,
41f3983a
JB
595 connectorMaximumPower / unitDivider,
596 connectorMinimumPower / unitDivider,
66a7748d
JB
597 {
598 limitationEnabled:
599 chargingStation.stationInfo?.customValueLimitationMeterValues,
600 fallbackValue: connectorMinimumPower / unitDivider
601 }
602 ),
603 powerSampledValueTemplate.fluctuationPercent ??
604 Constants.DEFAULT_FLUCTUATION_PERCENT
605 )
606 : getRandomFloatRounded(
607 connectorMaximumPower / unitDivider,
608 connectorMinimumPower / unitDivider
609 )
610 break
41f3983a 611 default:
66a7748d
JB
612 logger.error(`${chargingStation.logPrefix()} ${errMsg}`)
613 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES)
41f3983a
JB
614 }
615 meterValue.sampledValue.push(
66a7748d
JB
616 buildSampledValue(powerSampledValueTemplate, powerMeasurandValues.allPhases)
617 )
618 const sampledValuesIndex = meterValue.sampledValue.length - 1
619 const connectorMaximumPowerRounded = roundTo(connectorMaximumPower / unitDivider, 2)
620 const connectorMinimumPowerRounded = roundTo(connectorMinimumPower / unitDivider, 2)
41f3983a
JB
621 if (
622 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
623 connectorMaximumPowerRounded ||
624 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) <
625 connectorMinimumPowerRounded ||
626 debug
627 ) {
628 logger.error(
629 `${chargingStation.logPrefix()} MeterValues measurand ${
630 meterValue.sampledValue[sampledValuesIndex].measurand ??
631 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
632 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumPowerRounded}/${
633 meterValue.sampledValue[sampledValuesIndex].value
66a7748d
JB
634 }/${connectorMaximumPowerRounded}`
635 )
41f3983a
JB
636 }
637 for (
638 let phase = 1;
639 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
640 phase++
641 ) {
66a7748d 642 const phaseValue = `L${phase}-N`
41f3983a
JB
643 meterValue.sampledValue.push(
644 buildSampledValue(
645 powerPerPhaseSampledValueTemplates[
646 `L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates
647 ] ?? powerSampledValueTemplate,
648 powerMeasurandValues[`L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates],
649 undefined,
66a7748d
JB
650 phaseValue as MeterValuePhase
651 )
652 )
653 const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1
41f3983a
JB
654 const connectorMaximumPowerPerPhaseRounded = roundTo(
655 connectorMaximumPowerPerPhase / unitDivider,
66a7748d
JB
656 2
657 )
41f3983a
JB
658 const connectorMinimumPowerPerPhaseRounded = roundTo(
659 connectorMinimumPowerPerPhase / unitDivider,
66a7748d
JB
660 2
661 )
41f3983a
JB
662 if (
663 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
664 connectorMaximumPowerPerPhaseRounded ||
665 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
666 connectorMinimumPowerPerPhaseRounded ||
667 debug
668 ) {
669 logger.error(
670 `${chargingStation.logPrefix()} MeterValues measurand ${
671 meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ??
672 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
673 }: phase ${
674 meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
675 }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumPowerPerPhaseRounded}/${
676 meterValue.sampledValue[sampledValuesPerPhaseIndex].value
66a7748d
JB
677 }/${connectorMaximumPowerPerPhaseRounded}`
678 )
41f3983a
JB
679 }
680 }
681 }
682 // Current.Import measurand
683 currentSampledValueTemplate = getSampledValueTemplate(
684 chargingStation,
685 connectorId,
66a7748d
JB
686 MeterValueMeasurand.CURRENT_IMPORT
687 )
41f3983a
JB
688 if (chargingStation.getNumberOfPhases() === 3) {
689 currentPerPhaseSampledValueTemplates = {
690 L1: getSampledValueTemplate(
691 chargingStation,
692 connectorId,
693 MeterValueMeasurand.CURRENT_IMPORT,
66a7748d 694 MeterValuePhase.L1
41f3983a
JB
695 ),
696 L2: getSampledValueTemplate(
697 chargingStation,
698 connectorId,
699 MeterValueMeasurand.CURRENT_IMPORT,
66a7748d 700 MeterValuePhase.L2
41f3983a
JB
701 ),
702 L3: getSampledValueTemplate(
703 chargingStation,
704 connectorId,
705 MeterValueMeasurand.CURRENT_IMPORT,
66a7748d
JB
706 MeterValuePhase.L3
707 )
708 }
41f3983a 709 }
66a7748d
JB
710 if (currentSampledValueTemplate != null) {
711 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
712 checkMeasurandPowerDivider(chargingStation, currentSampledValueTemplate.measurand!)
41f3983a
JB
713 const errMsg = `MeterValues measurand ${
714 currentSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
715 }: Unknown ${chargingStation.stationInfo?.currentOutType} currentOutType in template file ${
716 chargingStation.templateFile
717 }, cannot calculate ${
718 currentSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
66a7748d
JB
719 } measurand value`
720 // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
721 const currentMeasurandValues: MeasurandValues = {} as MeasurandValues
41f3983a 722 const connectorMaximumAvailablePower =
66a7748d
JB
723 chargingStation.getConnectorMaximumAvailablePower(connectorId)
724 const connectorMinimumAmperage = currentSampledValueTemplate.minimumValue ?? 0
725 let connectorMaximumAmperage: number
41f3983a
JB
726 switch (chargingStation.stationInfo?.currentOutType) {
727 case CurrentType.AC:
728 connectorMaximumAmperage = ACElectricUtils.amperagePerPhaseFromPower(
729 chargingStation.getNumberOfPhases(),
730 connectorMaximumAvailablePower,
66a7748d
JB
731 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
732 chargingStation.stationInfo.voltageOut!
733 )
41f3983a
JB
734 if (chargingStation.getNumberOfPhases() === 3) {
735 const defaultFluctuatedAmperagePerPhase = isNotEmptyString(
66a7748d 736 currentSampledValueTemplate.value
41f3983a
JB
737 )
738 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
739 getLimitFromSampledValueTemplateCustomValue(
740 currentSampledValueTemplate.value,
741 connectorMaximumAmperage,
742 connectorMinimumAmperage,
743 {
744 limitationEnabled:
41f3983a 745 chargingStation.stationInfo?.customValueLimitationMeterValues,
66a7748d
JB
746 fallbackValue: connectorMinimumAmperage
747 }
748 ),
749 currentSampledValueTemplate.fluctuationPercent ??
750 Constants.DEFAULT_FLUCTUATION_PERCENT
751 )
752 : undefined
41f3983a 753 const phase1FluctuatedValue = isNotEmptyString(
66a7748d 754 currentPerPhaseSampledValueTemplates.L1?.value
41f3983a
JB
755 )
756 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
757 getLimitFromSampledValueTemplateCustomValue(
758 currentPerPhaseSampledValueTemplates.L1?.value,
759 connectorMaximumAmperage,
760 connectorMinimumAmperage,
761 {
762 limitationEnabled:
41f3983a 763 chargingStation.stationInfo?.customValueLimitationMeterValues,
66a7748d
JB
764 fallbackValue: connectorMinimumAmperage
765 }
766 ),
767 currentPerPhaseSampledValueTemplates.L1?.fluctuationPercent ??
768 Constants.DEFAULT_FLUCTUATION_PERCENT
769 )
770 : undefined
41f3983a 771 const phase2FluctuatedValue = isNotEmptyString(
66a7748d 772 currentPerPhaseSampledValueTemplates.L2?.value
41f3983a
JB
773 )
774 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
775 getLimitFromSampledValueTemplateCustomValue(
776 currentPerPhaseSampledValueTemplates.L2?.value,
777 connectorMaximumAmperage,
778 connectorMinimumAmperage,
779 {
780 limitationEnabled:
41f3983a 781 chargingStation.stationInfo?.customValueLimitationMeterValues,
66a7748d
JB
782 fallbackValue: connectorMinimumAmperage
783 }
784 ),
785 currentPerPhaseSampledValueTemplates.L2?.fluctuationPercent ??
786 Constants.DEFAULT_FLUCTUATION_PERCENT
787 )
788 : undefined
41f3983a 789 const phase3FluctuatedValue = isNotEmptyString(
66a7748d 790 currentPerPhaseSampledValueTemplates.L3?.value
41f3983a
JB
791 )
792 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
793 getLimitFromSampledValueTemplateCustomValue(
794 currentPerPhaseSampledValueTemplates.L3?.value,
795 connectorMaximumAmperage,
796 connectorMinimumAmperage,
797 {
798 limitationEnabled:
41f3983a 799 chargingStation.stationInfo?.customValueLimitationMeterValues,
66a7748d
JB
800 fallbackValue: connectorMinimumAmperage
801 }
802 ),
803 currentPerPhaseSampledValueTemplates.L3?.fluctuationPercent ??
804 Constants.DEFAULT_FLUCTUATION_PERCENT
805 )
806 : undefined
41f3983a
JB
807 currentMeasurandValues.L1 =
808 phase1FluctuatedValue ??
809 defaultFluctuatedAmperagePerPhase ??
66a7748d 810 getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage)
41f3983a
JB
811 currentMeasurandValues.L2 =
812 phase2FluctuatedValue ??
813 defaultFluctuatedAmperagePerPhase ??
66a7748d 814 getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage)
41f3983a
JB
815 currentMeasurandValues.L3 =
816 phase3FluctuatedValue ??
817 defaultFluctuatedAmperagePerPhase ??
66a7748d 818 getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage)
41f3983a
JB
819 } else {
820 currentMeasurandValues.L1 = isNotEmptyString(currentSampledValueTemplate.value)
821 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
822 getLimitFromSampledValueTemplateCustomValue(
823 currentSampledValueTemplate.value,
824 connectorMaximumAmperage,
825 connectorMinimumAmperage,
826 {
827 limitationEnabled:
41f3983a 828 chargingStation.stationInfo?.customValueLimitationMeterValues,
66a7748d
JB
829 fallbackValue: connectorMinimumAmperage
830 }
831 ),
832 currentSampledValueTemplate.fluctuationPercent ??
833 Constants.DEFAULT_FLUCTUATION_PERCENT
834 )
835 : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage)
836 currentMeasurandValues.L2 = 0
837 currentMeasurandValues.L3 = 0
41f3983a
JB
838 }
839 currentMeasurandValues.allPhases = roundTo(
840 (currentMeasurandValues.L1 + currentMeasurandValues.L2 + currentMeasurandValues.L3) /
841 chargingStation.getNumberOfPhases(),
66a7748d
JB
842 2
843 )
844 break
41f3983a
JB
845 case CurrentType.DC:
846 connectorMaximumAmperage = DCElectricUtils.amperage(
847 connectorMaximumAvailablePower,
66a7748d
JB
848 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
849 chargingStation.stationInfo.voltageOut!
850 )
41f3983a
JB
851 currentMeasurandValues.allPhases = isNotEmptyString(currentSampledValueTemplate.value)
852 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
853 getLimitFromSampledValueTemplateCustomValue(
854 currentSampledValueTemplate.value,
855 connectorMaximumAmperage,
856 connectorMinimumAmperage,
857 {
858 limitationEnabled:
41f3983a 859 chargingStation.stationInfo?.customValueLimitationMeterValues,
66a7748d
JB
860 fallbackValue: connectorMinimumAmperage
861 }
862 ),
863 currentSampledValueTemplate.fluctuationPercent ??
864 Constants.DEFAULT_FLUCTUATION_PERCENT
865 )
866 : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage)
867 break
41f3983a 868 default:
66a7748d
JB
869 logger.error(`${chargingStation.logPrefix()} ${errMsg}`)
870 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES)
41f3983a
JB
871 }
872 meterValue.sampledValue.push(
66a7748d
JB
873 buildSampledValue(currentSampledValueTemplate, currentMeasurandValues.allPhases)
874 )
875 const sampledValuesIndex = meterValue.sampledValue.length - 1
41f3983a
JB
876 if (
877 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
878 connectorMaximumAmperage ||
879 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) <
880 connectorMinimumAmperage ||
881 debug
882 ) {
883 logger.error(
884 `${chargingStation.logPrefix()} MeterValues measurand ${
885 meterValue.sampledValue[sampledValuesIndex].measurand ??
886 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
887 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${
888 meterValue.sampledValue[sampledValuesIndex].value
66a7748d
JB
889 }/${connectorMaximumAmperage}`
890 )
41f3983a
JB
891 }
892 for (
893 let phase = 1;
894 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
895 phase++
896 ) {
66a7748d 897 const phaseValue = `L${phase}`
41f3983a
JB
898 meterValue.sampledValue.push(
899 buildSampledValue(
900 currentPerPhaseSampledValueTemplates[
901 phaseValue as keyof MeasurandPerPhaseSampledValueTemplates
902 ] ?? currentSampledValueTemplate,
903 currentMeasurandValues[phaseValue as keyof MeasurandPerPhaseSampledValueTemplates],
904 undefined,
66a7748d
JB
905 phaseValue as MeterValuePhase
906 )
907 )
908 const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1
41f3983a
JB
909 if (
910 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
911 connectorMaximumAmperage ||
912 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
913 connectorMinimumAmperage ||
914 debug
915 ) {
916 logger.error(
917 `${chargingStation.logPrefix()} MeterValues measurand ${
918 meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ??
919 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
920 }: phase ${
921 meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
922 }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${
923 meterValue.sampledValue[sampledValuesPerPhaseIndex].value
66a7748d
JB
924 }/${connectorMaximumAmperage}`
925 )
41f3983a
JB
926 }
927 }
928 }
929 // Energy.Active.Import.Register measurand (default)
66a7748d
JB
930 energySampledValueTemplate = getSampledValueTemplate(chargingStation, connectorId)
931 if (energySampledValueTemplate != null) {
932 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
933 checkMeasurandPowerDivider(chargingStation, energySampledValueTemplate.measurand!)
41f3983a 934 const unitDivider =
66a7748d 935 energySampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1
41f3983a 936 const connectorMaximumAvailablePower =
66a7748d 937 chargingStation.getConnectorMaximumAvailablePower(connectorId)
41f3983a
JB
938 const connectorMaximumEnergyRounded = roundTo(
939 (connectorMaximumAvailablePower * interval) / (3600 * 1000),
66a7748d
JB
940 2
941 )
41f3983a
JB
942 const connectorMinimumEnergyRounded = roundTo(
943 energySampledValueTemplate.minimumValue ?? 0,
66a7748d
JB
944 2
945 )
41f3983a
JB
946 const energyValueRounded = isNotEmptyString(energySampledValueTemplate.value)
947 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
948 getLimitFromSampledValueTemplateCustomValue(
949 energySampledValueTemplate.value,
950 connectorMaximumEnergyRounded,
951 connectorMinimumEnergyRounded,
952 {
953 limitationEnabled: chargingStation.stationInfo?.customValueLimitationMeterValues,
954 fallbackValue: connectorMinimumEnergyRounded,
955 unitMultiplier: unitDivider
956 }
957 ),
958 energySampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT
959 )
960 : getRandomFloatRounded(connectorMaximumEnergyRounded, connectorMinimumEnergyRounded)
41f3983a 961 // Persist previous value on connector
66a7748d 962 if (connector != null) {
41f3983a 963 if (
66a7748d
JB
964 !isNullOrUndefined(connector.energyActiveImportRegisterValue) &&
965 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
41f3983a 966 connector.energyActiveImportRegisterValue! >= 0 &&
66a7748d
JB
967 !isNullOrUndefined(connector.transactionEnergyActiveImportRegisterValue) &&
968 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
41f3983a
JB
969 connector.transactionEnergyActiveImportRegisterValue! >= 0
970 ) {
66a7748d
JB
971 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
972 connector.energyActiveImportRegisterValue! += energyValueRounded
973 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
974 connector.transactionEnergyActiveImportRegisterValue! += energyValueRounded
41f3983a 975 } else {
66a7748d
JB
976 connector.energyActiveImportRegisterValue = 0
977 connector.transactionEnergyActiveImportRegisterValue = 0
41f3983a
JB
978 }
979 }
980 meterValue.sampledValue.push(
981 buildSampledValue(
982 energySampledValueTemplate,
983 roundTo(
984 chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) /
985 unitDivider,
66a7748d
JB
986 2
987 )
988 )
989 )
990 const sampledValuesIndex = meterValue.sampledValue.length - 1
41f3983a
JB
991 if (
992 energyValueRounded > connectorMaximumEnergyRounded ||
993 energyValueRounded < connectorMinimumEnergyRounded ||
994 debug
995 ) {
996 logger.error(
997 `${chargingStation.logPrefix()} MeterValues measurand ${
998 meterValue.sampledValue[sampledValuesIndex].measurand ??
999 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
66a7748d
JB
1000 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumEnergyRounded}/${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${interval}ms`
1001 )
41f3983a
JB
1002 }
1003 }
66a7748d 1004 return meterValue
41f3983a
JB
1005 case OCPPVersion.VERSION_20:
1006 case OCPPVersion.VERSION_201:
1007 default:
1008 throw new BaseError(
66a7748d
JB
1009 `Cannot build meterValue: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`
1010 )
41f3983a 1011 }
66a7748d 1012}
41f3983a
JB
1013
1014export const buildTransactionEndMeterValue = (
1015 chargingStation: ChargingStation,
1016 connectorId: number,
66a7748d 1017 meterStop: number
41f3983a 1018): MeterValue => {
66a7748d
JB
1019 let meterValue: MeterValue
1020 let sampledValueTemplate: SampledValueTemplate | undefined
1021 let unitDivider: number
41f3983a
JB
1022 switch (chargingStation.stationInfo?.ocppVersion) {
1023 case OCPPVersion.VERSION_16:
1024 meterValue = {
1025 timestamp: new Date(),
66a7748d
JB
1026 sampledValue: []
1027 }
41f3983a 1028 // Energy.Active.Import.Register measurand (default)
66a7748d
JB
1029 sampledValueTemplate = getSampledValueTemplate(chargingStation, connectorId)
1030 unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1
41f3983a
JB
1031 meterValue.sampledValue.push(
1032 buildSampledValue(
66a7748d 1033 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
41f3983a
JB
1034 sampledValueTemplate!,
1035 roundTo((meterStop ?? 0) / unitDivider, 4),
66a7748d
JB
1036 MeterValueContext.TRANSACTION_END
1037 )
1038 )
1039 return meterValue
41f3983a
JB
1040 case OCPPVersion.VERSION_20:
1041 case OCPPVersion.VERSION_201:
1042 default:
1043 throw new BaseError(
66a7748d
JB
1044 `Cannot build meterValue: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`
1045 )
41f3983a 1046 }
66a7748d 1047}
41f3983a
JB
1048
1049const checkMeasurandPowerDivider = (
1050 chargingStation: ChargingStation,
66a7748d 1051 measurandType: MeterValueMeasurand
41f3983a
JB
1052): void => {
1053 if (isUndefined(chargingStation.powerDivider)) {
1054 const errMsg = `MeterValues measurand ${
1055 measurandType ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
66a7748d
JB
1056 }: powerDivider is undefined`
1057 logger.error(`${chargingStation.logPrefix()} ${errMsg}`)
1058 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES)
41f3983a
JB
1059 } else if (chargingStation?.powerDivider <= 0) {
1060 const errMsg = `MeterValues measurand ${
1061 measurandType ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
66a7748d
JB
1062 }: powerDivider have zero or below value ${chargingStation.powerDivider}`
1063 logger.error(`${chargingStation.logPrefix()} ${errMsg}`)
1064 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES)
41f3983a 1065 }
66a7748d 1066}
41f3983a
JB
1067
1068const getLimitFromSampledValueTemplateCustomValue = (
1069 value: string | undefined,
1070 maxLimit: number,
1071 minLimit: number,
66a7748d 1072 options?: { limitationEnabled?: boolean, fallbackValue?: number, unitMultiplier?: number }
41f3983a
JB
1073): number => {
1074 options = {
1075 ...{
1076 limitationEnabled: false,
1077 unitMultiplier: 1,
66a7748d 1078 fallbackValue: 0
41f3983a 1079 },
66a7748d
JB
1080 ...options
1081 }
1082 const parsedValue = parseInt(value ?? '')
1083 if (options?.limitationEnabled === true) {
41f3983a 1084 return max(
66a7748d 1085 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
41f3983a 1086 min((!isNaN(parsedValue) ? parsedValue : Infinity) * options.unitMultiplier!, maxLimit),
66a7748d
JB
1087 minLimit
1088 )
41f3983a 1089 }
66a7748d
JB
1090 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1091 return (!isNaN(parsedValue) ? parsedValue : options.fallbackValue!) * options.unitMultiplier!
1092}
41f3983a
JB
1093
1094const getSampledValueTemplate = (
1095 chargingStation: ChargingStation,
1096 connectorId: number,
1097 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
66a7748d 1098 phase?: MeterValuePhase
41f3983a 1099): SampledValueTemplate | undefined => {
66a7748d
JB
1100 const onPhaseStr = phase != null ? `on phase ${phase} ` : ''
1101 if (!OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(measurand)) {
41f3983a 1102 logger.warn(
66a7748d
JB
1103 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
1104 )
1105 return
41f3983a
JB
1106 }
1107 if (
1108 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
1109 getConfigurationKey(
1110 chargingStation,
66a7748d 1111 StandardParametersKey.MeterValuesSampledData
41f3983a
JB
1112 )?.value?.includes(measurand) === false
1113 ) {
1114 logger.debug(
1115 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
1116 StandardParametersKey.MeterValuesSampledData
66a7748d
JB
1117 }' OCPP parameter`
1118 )
1119 return
41f3983a
JB
1120 }
1121 const sampledValueTemplates: SampledValueTemplate[] =
66a7748d
JB
1122 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1123 chargingStation.getConnectorStatus(connectorId)!.MeterValues
41f3983a
JB
1124 for (
1125 let index = 0;
66a7748d 1126 isNotEmptyArray(sampledValueTemplates) && index < sampledValueTemplates.length;
41f3983a
JB
1127 index++
1128 ) {
1129 if (
66a7748d
JB
1130 !OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(
1131 sampledValueTemplates[index]?.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1132 )
41f3983a
JB
1133 ) {
1134 logger.warn(
66a7748d
JB
1135 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
1136 )
41f3983a 1137 } else if (
66a7748d 1138 phase != null &&
41f3983a
JB
1139 sampledValueTemplates[index]?.phase === phase &&
1140 sampledValueTemplates[index]?.measurand === measurand &&
1141 getConfigurationKey(
1142 chargingStation,
66a7748d 1143 StandardParametersKey.MeterValuesSampledData
41f3983a
JB
1144 )?.value?.includes(measurand) === true
1145 ) {
66a7748d 1146 return sampledValueTemplates[index]
41f3983a 1147 } else if (
66a7748d
JB
1148 phase == null &&
1149 sampledValueTemplates[index]?.phase == null &&
41f3983a
JB
1150 sampledValueTemplates[index]?.measurand === measurand &&
1151 getConfigurationKey(
1152 chargingStation,
66a7748d 1153 StandardParametersKey.MeterValuesSampledData
41f3983a
JB
1154 )?.value?.includes(measurand) === true
1155 ) {
66a7748d 1156 return sampledValueTemplates[index]
41f3983a
JB
1157 } else if (
1158 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
66a7748d 1159 (sampledValueTemplates[index]?.measurand == null ||
41f3983a
JB
1160 sampledValueTemplates[index]?.measurand === measurand)
1161 ) {
66a7748d 1162 return sampledValueTemplates[index]
41f3983a
JB
1163 }
1164 }
1165 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
66a7748d
JB
1166 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`
1167 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`)
1168 throw new BaseError(errorMsg)
41f3983a
JB
1169 }
1170 logger.debug(
66a7748d
JB
1171 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
1172 )
1173}
41f3983a
JB
1174
1175const buildSampledValue = (
1176 sampledValueTemplate: SampledValueTemplate,
1177 value: number,
1178 context?: MeterValueContext,
66a7748d 1179 phase?: MeterValuePhase
41f3983a 1180): SampledValue => {
66a7748d 1181 const sampledValueContext = context ?? sampledValueTemplate?.context
41f3983a 1182 const sampledValueLocation =
66a7748d
JB
1183 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1184 sampledValueTemplate?.location ?? getMeasurandDefaultLocation(sampledValueTemplate.measurand!)
1185 const sampledValuePhase = phase ?? sampledValueTemplate?.phase
1186 // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
41f3983a
JB
1187 return {
1188 ...(!isNullOrUndefined(sampledValueTemplate.unit) && {
66a7748d 1189 unit: sampledValueTemplate.unit
41f3983a
JB
1190 }),
1191 ...(!isNullOrUndefined(sampledValueContext) && { context: sampledValueContext }),
1192 ...(!isNullOrUndefined(sampledValueTemplate.measurand) && {
66a7748d 1193 measurand: sampledValueTemplate.measurand
41f3983a
JB
1194 }),
1195 ...(!isNullOrUndefined(sampledValueLocation) && { location: sampledValueLocation }),
1196 ...(!isNullOrUndefined(value) && { value: value.toString() }),
66a7748d
JB
1197 ...(!isNullOrUndefined(sampledValuePhase) && { phase: sampledValuePhase })
1198 } as SampledValue
1199}
41f3983a
JB
1200
1201const getMeasurandDefaultLocation = (
66a7748d 1202 measurandType: MeterValueMeasurand
41f3983a
JB
1203): MeterValueLocation | undefined => {
1204 switch (measurandType) {
1205 case MeterValueMeasurand.STATE_OF_CHARGE:
66a7748d 1206 return MeterValueLocation.EV
41f3983a 1207 }
66a7748d 1208}
41f3983a
JB
1209
1210// const getMeasurandDefaultUnit = (
66a7748d 1211// measurandType: MeterValueMeasurand
41f3983a
JB
1212// ): MeterValueUnit | undefined => {
1213// switch (measurandType) {
1214// case MeterValueMeasurand.CURRENT_EXPORT:
1215// case MeterValueMeasurand.CURRENT_IMPORT:
1216// case MeterValueMeasurand.CURRENT_OFFERED:
66a7748d 1217// return MeterValueUnit.AMP
41f3983a
JB
1218// case MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER:
1219// case MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER:
66a7748d 1220// return MeterValueUnit.WATT_HOUR
41f3983a
JB
1221// case MeterValueMeasurand.POWER_ACTIVE_EXPORT:
1222// case MeterValueMeasurand.POWER_ACTIVE_IMPORT:
1223// case MeterValueMeasurand.POWER_OFFERED:
66a7748d 1224// return MeterValueUnit.WATT
41f3983a 1225// case MeterValueMeasurand.STATE_OF_CHARGE:
66a7748d 1226// return MeterValueUnit.PERCENT
41f3983a 1227// case MeterValueMeasurand.VOLTAGE:
66a7748d 1228// return MeterValueUnit.VOLT
41f3983a 1229// }
66a7748d 1230// }
41f3983a 1231
66a7748d 1232// eslint-disable-next-line @typescript-eslint/no-extraneous-class
90befdb8 1233export class OCPPServiceUtils {
66a7748d
JB
1234 public static getMessageTypeString = getMessageTypeString
1235 public static sendAndSetConnectorStatus = sendAndSetConnectorStatus
1236 public static isIdTagAuthorized = isIdTagAuthorized
1237 public static buildTransactionEndMeterValue = buildTransactionEndMeterValue
1238 protected static getSampledValueTemplate = getSampledValueTemplate
1239 protected static buildSampledValue = buildSampledValue
041365be 1240
66a7748d 1241 protected constructor () {
d5bd1c00
JB
1242 // This is intentional
1243 }
1244
66a7748d
JB
1245 public static ajvErrorsToErrorType (errors: ErrorObject[] | null | undefined): ErrorType {
1246 if (isNotEmptyArray(errors)) {
9ff486f4
JB
1247 for (const error of errors as DefinedError[]) {
1248 switch (error.keyword) {
1249 case 'type':
66a7748d 1250 return ErrorType.TYPE_CONSTRAINT_VIOLATION
9ff486f4
JB
1251 case 'dependencies':
1252 case 'required':
66a7748d 1253 return ErrorType.OCCURRENCE_CONSTRAINT_VIOLATION
9ff486f4
JB
1254 case 'pattern':
1255 case 'format':
66a7748d 1256 return ErrorType.PROPERTY_CONSTRAINT_VIOLATION
9ff486f4 1257 }
06ad945f
JB
1258 }
1259 }
66a7748d 1260 return ErrorType.FORMAT_VIOLATION
06ad945f
JB
1261 }
1262
66a7748d 1263 public static isRequestCommandSupported (
fd3c56d1 1264 chargingStation: ChargingStation,
66a7748d 1265 command: RequestCommand
ed3d2808 1266 ): boolean {
66a7748d 1267 const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command)
ed3d2808 1268 if (
66a7748d
JB
1269 isRequestCommand &&
1270 chargingStation.stationInfo?.commandsSupport?.outgoingCommands == null
ed3d2808 1271 ) {
66a7748d 1272 return true
ed3d2808 1273 } else if (
66a7748d
JB
1274 isRequestCommand &&
1275 chargingStation.stationInfo?.commandsSupport?.outgoingCommands?.[command] != null
ed3d2808 1276 ) {
66a7748d 1277 return chargingStation.stationInfo?.commandsSupport?.outgoingCommands[command]
ed3d2808 1278 }
66a7748d
JB
1279 logger.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`)
1280 return false
ed3d2808
JB
1281 }
1282
66a7748d 1283 public static isIncomingRequestCommandSupported (
fd3c56d1 1284 chargingStation: ChargingStation,
66a7748d 1285 command: IncomingRequestCommand
ed3d2808 1286 ): boolean {
edd13439 1287 const isIncomingRequestCommand =
66a7748d 1288 Object.values<IncomingRequestCommand>(IncomingRequestCommand).includes(command)
ed3d2808 1289 if (
66a7748d
JB
1290 isIncomingRequestCommand &&
1291 chargingStation.stationInfo?.commandsSupport?.incomingCommands == null
ed3d2808 1292 ) {
66a7748d 1293 return true
ed3d2808 1294 } else if (
66a7748d
JB
1295 isIncomingRequestCommand &&
1296 chargingStation.stationInfo?.commandsSupport?.incomingCommands?.[command] != null
ed3d2808 1297 ) {
66a7748d 1298 return chargingStation.stationInfo?.commandsSupport?.incomingCommands[command]
ed3d2808 1299 }
66a7748d
JB
1300 logger.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`)
1301 return false
ed3d2808
JB
1302 }
1303
66a7748d 1304 public static isMessageTriggerSupported (
c60ed4b8 1305 chargingStation: ChargingStation,
66a7748d 1306 messageTrigger: MessageTrigger
c60ed4b8 1307 ): boolean {
66a7748d
JB
1308 const isMessageTrigger = Object.values(MessageTrigger).includes(messageTrigger)
1309 if (isMessageTrigger && chargingStation.stationInfo?.messageTriggerSupport == null) {
1310 return true
1c9de2b9 1311 } else if (
66a7748d
JB
1312 isMessageTrigger &&
1313 chargingStation.stationInfo?.messageTriggerSupport?.[messageTrigger] != null
1c9de2b9 1314 ) {
66a7748d 1315 return chargingStation.stationInfo?.messageTriggerSupport[messageTrigger]
c60ed4b8
JB
1316 }
1317 logger.error(
66a7748d
JB
1318 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`
1319 )
1320 return false
c60ed4b8
JB
1321 }
1322
66a7748d 1323 public static isConnectorIdValid (
c60ed4b8
JB
1324 chargingStation: ChargingStation,
1325 ocppCommand: IncomingRequestCommand,
66a7748d 1326 connectorId: number
c60ed4b8
JB
1327 ): boolean {
1328 if (connectorId < 0) {
1329 logger.error(
66a7748d
JB
1330 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`
1331 )
1332 return false
c60ed4b8 1333 }
66a7748d 1334 return true
c60ed4b8
JB
1335 }
1336
1799761a 1337 public static convertDateToISOString<T extends JsonType>(obj: T): void {
a37fc6dc 1338 for (const key in obj) {
66a7748d 1339 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-non-null-assertion
3dcf7b67 1340 if (isDate(obj![key])) {
66a7748d
JB
1341 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-non-null-assertion
1342 (obj![key] as string) = (obj![key] as Date).toISOString()
1343 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-non-null-assertion
e1d9a0f4 1344 } else if (obj![key] !== null && typeof obj![key] === 'object') {
66a7748d
JB
1345 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-non-null-assertion
1346 OCPPServiceUtils.convertDateToISOString<T>(obj![key] as T)
1799761a
JB
1347 }
1348 }
1349 }
1350
66a7748d 1351 public static startHeartbeatInterval (chargingStation: ChargingStation, interval: number): void {
a807045b 1352 if (chargingStation.heartbeatSetInterval == null) {
66a7748d 1353 chargingStation.startHeartbeat()
8f953431 1354 } else if (chargingStation.getHeartbeatInterval() !== interval) {
66a7748d 1355 chargingStation.restartHeartbeat()
8f953431
JB
1356 }
1357 }
1358
7164966d 1359 protected static parseJsonSchemaFile<T extends JsonType>(
51022aa0 1360 relativePath: string,
1b271a54
JB
1361 ocppVersion: OCPPVersion,
1362 moduleName?: string,
66a7748d 1363 methodName?: string
7164966d 1364 ): JSONSchemaType<T> {
66a7748d 1365 const filePath = join(dirname(fileURLToPath(import.meta.url)), relativePath)
7164966d 1366 try {
66a7748d 1367 return JSON.parse(readFileSync(filePath, 'utf8')) as JSONSchemaType<T>
7164966d 1368 } catch (error) {
fa5995d6 1369 handleFileException(
7164966d
JB
1370 filePath,
1371 FileType.JsonSchema,
1372 error as NodeJS.ErrnoException,
1b271a54 1373 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
66a7748d
JB
1374 { throwError: false }
1375 )
1376 // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
1377 return {} as JSONSchemaType<T>
7164966d 1378 }
130783a7
JB
1379 }
1380
66a7748d 1381 private static readonly logPrefix = (
1b271a54
JB
1382 ocppVersion: OCPPVersion,
1383 moduleName?: string,
66a7748d 1384 methodName?: string
1b271a54
JB
1385 ): string => {
1386 const logMsg =
9bf0ef23 1387 isNotEmptyString(moduleName) && isNotEmptyString(methodName)
1b271a54 1388 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
66a7748d
JB
1389 : ` OCPP ${ocppVersion} |`
1390 return logPrefix(logMsg)
1391 }
90befdb8 1392}