refactor: cleanup some unneeded conditions
[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,
268a74bb 23 type ConnectorStatusEnum,
41f3983a 24 CurrentType,
268a74bb
JB
25 ErrorType,
26 FileType,
6e939d9e 27 IncomingRequestCommand,
268a74bb 28 type JsonType,
41f3983a
JB
29 type MeasurandPerPhaseSampledValueTemplates,
30 type MeasurandValues,
6e939d9e 31 MessageTrigger,
268a74bb 32 MessageType,
41f3983a
JB
33 type MeterValue,
34 MeterValueContext,
35 MeterValueLocation,
268a74bb 36 MeterValueMeasurand,
41f3983a
JB
37 MeterValuePhase,
38 MeterValueUnit,
cc6845fc 39 type OCPP16ChargePointStatus,
268a74bb 40 type OCPP16StatusNotificationRequest,
cc6845fc 41 type OCPP20ConnectorStatusEnumType,
268a74bb
JB
42 type OCPP20StatusNotificationRequest,
43 OCPPVersion,
6e939d9e 44 RequestCommand,
41f3983a 45 type SampledValue,
268a74bb
JB
46 type SampledValueTemplate,
47 StandardParametersKey,
6e939d9e 48 type StatusNotificationRequest,
66a7748d
JB
49 type StatusNotificationResponse
50} from '../../types/index.js'
9bf0ef23 51import {
41f3983a
JB
52 ACElectricUtils,
53 Constants,
54 DCElectricUtils,
55 convertToFloat,
56 convertToInt,
57 getRandomFloatFluctuatedRounded,
58 getRandomFloatRounded,
59 getRandomInteger,
9bf0ef23
JB
60 handleFileException,
61 isNotEmptyArray,
62 isNotEmptyString,
63 logPrefix,
64 logger,
d71ce3fa 65 max,
5adf6ca4 66 min,
66a7748d
JB
67 roundTo
68} from '../../utils/index.js'
06ad945f 69
041365be
JB
70export const getMessageTypeString = (messageType: MessageType): string => {
71 switch (messageType) {
72 case MessageType.CALL_MESSAGE:
66a7748d 73 return 'request'
041365be 74 case MessageType.CALL_RESULT_MESSAGE:
66a7748d 75 return 'response'
041365be 76 case MessageType.CALL_ERROR_MESSAGE:
66a7748d 77 return 'error'
041365be 78 default:
66a7748d 79 return 'unknown'
041365be 80 }
66a7748d 81}
041365be
JB
82
83export const buildStatusNotificationRequest = (
84 chargingStation: ChargingStation,
85 connectorId: number,
86 status: ConnectorStatusEnum,
66a7748d 87 evseId?: number
041365be
JB
88): StatusNotificationRequest => {
89 switch (chargingStation.stationInfo?.ocppVersion) {
90 case OCPPVersion.VERSION_16:
91 return {
92 connectorId,
cc6845fc 93 status: status as OCPP16ChargePointStatus,
66a7748d
JB
94 errorCode: ChargePointErrorCode.NO_ERROR
95 } satisfies OCPP16StatusNotificationRequest
041365be
JB
96 case OCPPVersion.VERSION_20:
97 case OCPPVersion.VERSION_201:
98 return {
99 timestamp: new Date(),
cc6845fc 100 connectorStatus: status as OCPP20ConnectorStatusEnumType,
041365be 101 connectorId,
cc6845fc
JB
102 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
103 evseId: evseId!
66a7748d 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
2466918c 125 const connectorStatus = chargingStation.getConnectorStatus(connectorId)!
66a7748d
JB
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
5199f9fd 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 162 )
5199f9fd 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 238 logger.warn(
5199f9fd
JB
239 `${chargingStation.logPrefix()} OCPP ${
240 chargingStation.stationInfo.ocppVersion
241 } 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 &&
5199f9fd 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 )
5199f9fd 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
5199f9fd 446 checkMeasurandPowerDivider(chargingStation, powerSampledValueTemplate.measurand)
41f3983a
JB
447 const errMsg = `MeterValues measurand ${
448 powerSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
5199f9fd 449 }: Unknown ${chargingStation.stationInfo.currentOutType} currentOutType in template file ${
41f3983a
JB
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
5199f9fd 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 )
5199f9fd 467 switch (chargingStation.stationInfo.currentOutType) {
41f3983a
JB
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:
5199f9fd 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:
5199f9fd 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:
5199f9fd 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:
5199f9fd 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:
5199f9fd 572 chargingStation.stationInfo.customValueLimitationMeterValues,
66a7748d
JB
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:
5199f9fd 600 chargingStation.stationInfo.customValueLimitationMeterValues,
66a7748d
JB
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
5199f9fd 713 checkMeasurandPowerDivider(chargingStation, currentSampledValueTemplate.measurand)
41f3983a
JB
714 const errMsg = `MeterValues measurand ${
715 currentSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
5199f9fd 716 }: Unknown ${chargingStation.stationInfo.currentOutType} currentOutType in template file ${
41f3983a
JB
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
5199f9fd 727 switch (chargingStation.stationInfo.currentOutType) {
41f3983a
JB
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:
5199f9fd 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:
5199f9fd 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:
5199f9fd 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:
5199f9fd 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:
5199f9fd 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:
5199f9fd 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
5199f9fd 934 checkMeasurandPowerDivider(chargingStation, energySampledValueTemplate.measurand)
41f3983a 935 const unitDivider =
5199f9fd 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 {
5199f9fd 954 limitationEnabled: chargingStation.stationInfo.customValueLimitationMeterValues,
66a7748d
JB
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,
5199f9fd 1014 meterStop: number | undefined
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,
5199f9fd 1048 measurandType: MeterValueMeasurand | undefined
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)
5199f9fd 1056 } else if (chargingStation.powerDivider <= 0) {
41f3983a
JB
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 ?? '')
5199f9fd 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 => {
5199f9fd 1178 const sampledValueContext = context ?? sampledValueTemplate.context
41f3983a 1179 const sampledValueLocation =
66a7748d 1180 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5199f9fd
JB
1181 sampledValueTemplate.location ?? getMeasurandDefaultLocation(sampledValueTemplate.measurand!)
1182 const sampledValuePhase = phase ?? sampledValueTemplate.phase
41f3983a 1183 return {
be9f397b 1184 ...(sampledValueTemplate.unit != null && {
66a7748d 1185 unit: sampledValueTemplate.unit
41f3983a 1186 }),
be9f397b
JB
1187 ...(sampledValueContext != null && { context: sampledValueContext }),
1188 ...(sampledValueTemplate.measurand != null && {
66a7748d 1189 measurand: sampledValueTemplate.measurand
41f3983a 1190 }),
be9f397b 1191 ...(sampledValueLocation != null && { location: sampledValueLocation }),
5199f9fd 1192 ...{ value: value.toString() },
be9f397b 1193 ...(sampledValuePhase != null && { phase: sampledValuePhase })
5199f9fd 1194 } satisfies SampledValue
66a7748d 1195}
41f3983a
JB
1196
1197const getMeasurandDefaultLocation = (
66a7748d 1198 measurandType: MeterValueMeasurand
41f3983a
JB
1199): MeterValueLocation | undefined => {
1200 switch (measurandType) {
1201 case MeterValueMeasurand.STATE_OF_CHARGE:
66a7748d 1202 return MeterValueLocation.EV
41f3983a 1203 }
66a7748d 1204}
41f3983a
JB
1205
1206// const getMeasurandDefaultUnit = (
66a7748d 1207// measurandType: MeterValueMeasurand
41f3983a
JB
1208// ): MeterValueUnit | undefined => {
1209// switch (measurandType) {
1210// case MeterValueMeasurand.CURRENT_EXPORT:
1211// case MeterValueMeasurand.CURRENT_IMPORT:
1212// case MeterValueMeasurand.CURRENT_OFFERED:
66a7748d 1213// return MeterValueUnit.AMP
41f3983a
JB
1214// case MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER:
1215// case MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER:
66a7748d 1216// return MeterValueUnit.WATT_HOUR
41f3983a
JB
1217// case MeterValueMeasurand.POWER_ACTIVE_EXPORT:
1218// case MeterValueMeasurand.POWER_ACTIVE_IMPORT:
1219// case MeterValueMeasurand.POWER_OFFERED:
66a7748d 1220// return MeterValueUnit.WATT
41f3983a 1221// case MeterValueMeasurand.STATE_OF_CHARGE:
66a7748d 1222// return MeterValueUnit.PERCENT
41f3983a 1223// case MeterValueMeasurand.VOLTAGE:
66a7748d 1224// return MeterValueUnit.VOLT
41f3983a 1225// }
66a7748d 1226// }
41f3983a 1227
66a7748d 1228// eslint-disable-next-line @typescript-eslint/no-extraneous-class
90befdb8 1229export class OCPPServiceUtils {
66a7748d
JB
1230 public static getMessageTypeString = getMessageTypeString
1231 public static sendAndSetConnectorStatus = sendAndSetConnectorStatus
1232 public static isIdTagAuthorized = isIdTagAuthorized
1233 public static buildTransactionEndMeterValue = buildTransactionEndMeterValue
1234 protected static getSampledValueTemplate = getSampledValueTemplate
1235 protected static buildSampledValue = buildSampledValue
041365be 1236
66a7748d 1237 protected constructor () {
d5bd1c00
JB
1238 // This is intentional
1239 }
1240
66a7748d
JB
1241 public static ajvErrorsToErrorType (errors: ErrorObject[] | null | undefined): ErrorType {
1242 if (isNotEmptyArray(errors)) {
9ff486f4
JB
1243 for (const error of errors as DefinedError[]) {
1244 switch (error.keyword) {
1245 case 'type':
66a7748d 1246 return ErrorType.TYPE_CONSTRAINT_VIOLATION
9ff486f4
JB
1247 case 'dependencies':
1248 case 'required':
66a7748d 1249 return ErrorType.OCCURRENCE_CONSTRAINT_VIOLATION
9ff486f4
JB
1250 case 'pattern':
1251 case 'format':
66a7748d 1252 return ErrorType.PROPERTY_CONSTRAINT_VIOLATION
9ff486f4 1253 }
06ad945f
JB
1254 }
1255 }
66a7748d 1256 return ErrorType.FORMAT_VIOLATION
06ad945f
JB
1257 }
1258
66a7748d 1259 public static isRequestCommandSupported (
fd3c56d1 1260 chargingStation: ChargingStation,
66a7748d 1261 command: RequestCommand
ed3d2808 1262 ): boolean {
66a7748d 1263 const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command)
ed3d2808 1264 if (
66a7748d
JB
1265 isRequestCommand &&
1266 chargingStation.stationInfo?.commandsSupport?.outgoingCommands == null
ed3d2808 1267 ) {
66a7748d 1268 return true
ed3d2808 1269 } else if (
66a7748d
JB
1270 isRequestCommand &&
1271 chargingStation.stationInfo?.commandsSupport?.outgoingCommands?.[command] != null
ed3d2808 1272 ) {
5199f9fd 1273 return chargingStation.stationInfo.commandsSupport.outgoingCommands[command]
ed3d2808 1274 }
66a7748d
JB
1275 logger.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`)
1276 return false
ed3d2808
JB
1277 }
1278
66a7748d 1279 public static isIncomingRequestCommandSupported (
fd3c56d1 1280 chargingStation: ChargingStation,
66a7748d 1281 command: IncomingRequestCommand
ed3d2808 1282 ): boolean {
edd13439 1283 const isIncomingRequestCommand =
66a7748d 1284 Object.values<IncomingRequestCommand>(IncomingRequestCommand).includes(command)
ed3d2808 1285 if (
66a7748d
JB
1286 isIncomingRequestCommand &&
1287 chargingStation.stationInfo?.commandsSupport?.incomingCommands == null
ed3d2808 1288 ) {
66a7748d 1289 return true
ed3d2808 1290 } else if (
66a7748d 1291 isIncomingRequestCommand &&
5199f9fd 1292 chargingStation.stationInfo?.commandsSupport?.incomingCommands[command] != null
ed3d2808 1293 ) {
5199f9fd 1294 return chargingStation.stationInfo.commandsSupport.incomingCommands[command]
ed3d2808 1295 }
66a7748d
JB
1296 logger.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`)
1297 return false
ed3d2808
JB
1298 }
1299
66a7748d 1300 public static isMessageTriggerSupported (
c60ed4b8 1301 chargingStation: ChargingStation,
66a7748d 1302 messageTrigger: MessageTrigger
c60ed4b8 1303 ): boolean {
66a7748d
JB
1304 const isMessageTrigger = Object.values(MessageTrigger).includes(messageTrigger)
1305 if (isMessageTrigger && chargingStation.stationInfo?.messageTriggerSupport == null) {
1306 return true
1c9de2b9 1307 } else if (
66a7748d
JB
1308 isMessageTrigger &&
1309 chargingStation.stationInfo?.messageTriggerSupport?.[messageTrigger] != null
1c9de2b9 1310 ) {
5199f9fd 1311 return chargingStation.stationInfo.messageTriggerSupport[messageTrigger]
c60ed4b8
JB
1312 }
1313 logger.error(
66a7748d
JB
1314 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`
1315 )
1316 return false
c60ed4b8
JB
1317 }
1318
66a7748d 1319 public static isConnectorIdValid (
c60ed4b8
JB
1320 chargingStation: ChargingStation,
1321 ocppCommand: IncomingRequestCommand,
66a7748d 1322 connectorId: number
c60ed4b8
JB
1323 ): boolean {
1324 if (connectorId < 0) {
1325 logger.error(
66a7748d
JB
1326 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`
1327 )
1328 return false
c60ed4b8 1329 }
66a7748d 1330 return true
c60ed4b8
JB
1331 }
1332
1799761a 1333 public static convertDateToISOString<T extends JsonType>(obj: T): void {
a37fc6dc 1334 for (const key in obj) {
66a7748d 1335 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-non-null-assertion
3dcf7b67 1336 if (isDate(obj![key])) {
66a7748d
JB
1337 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-non-null-assertion
1338 (obj![key] as string) = (obj![key] as Date).toISOString()
5199f9fd
JB
1339 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-condition
1340 } else if (typeof obj![key] === 'object' && obj![key] !== null) {
66a7748d
JB
1341 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-non-null-assertion
1342 OCPPServiceUtils.convertDateToISOString<T>(obj![key] as T)
1799761a
JB
1343 }
1344 }
1345 }
1346
66a7748d 1347 public static startHeartbeatInterval (chargingStation: ChargingStation, interval: number): void {
a807045b 1348 if (chargingStation.heartbeatSetInterval == null) {
66a7748d 1349 chargingStation.startHeartbeat()
8f953431 1350 } else if (chargingStation.getHeartbeatInterval() !== interval) {
66a7748d 1351 chargingStation.restartHeartbeat()
8f953431
JB
1352 }
1353 }
1354
7164966d 1355 protected static parseJsonSchemaFile<T extends JsonType>(
51022aa0 1356 relativePath: string,
1b271a54
JB
1357 ocppVersion: OCPPVersion,
1358 moduleName?: string,
66a7748d 1359 methodName?: string
7164966d 1360 ): JSONSchemaType<T> {
66a7748d 1361 const filePath = join(dirname(fileURLToPath(import.meta.url)), relativePath)
7164966d 1362 try {
66a7748d 1363 return JSON.parse(readFileSync(filePath, 'utf8')) as JSONSchemaType<T>
7164966d 1364 } catch (error) {
fa5995d6 1365 handleFileException(
7164966d
JB
1366 filePath,
1367 FileType.JsonSchema,
1368 error as NodeJS.ErrnoException,
1b271a54 1369 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
66a7748d
JB
1370 { throwError: false }
1371 )
1372 // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
1373 return {} as JSONSchemaType<T>
7164966d 1374 }
130783a7
JB
1375 }
1376
66a7748d 1377 private static readonly logPrefix = (
1b271a54
JB
1378 ocppVersion: OCPPVersion,
1379 moduleName?: string,
66a7748d 1380 methodName?: string
1b271a54
JB
1381 ): string => {
1382 const logMsg =
9bf0ef23 1383 isNotEmptyString(moduleName) && isNotEmptyString(methodName)
1b271a54 1384 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
66a7748d
JB
1385 : ` OCPP ${ocppVersion} |`
1386 return logPrefix(logMsg)
1387 }
90befdb8 1388}