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