refactor: switch eslint configuration to strict type checking
[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
5199f9fd 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 163 )
5199f9fd 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 239 logger.warn(
5199f9fd
JB
240 `${chargingStation.logPrefix()} OCPP ${
241 chargingStation.stationInfo.ocppVersion
242 } connector id ${connectorId} status transition from '${
66a7748d 243 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
041365be 244 chargingStation.getConnectorStatus(connectorId)!.status
66a7748d
JB
245 }' to '${status}' is not allowed`
246 )
041365be 247 }
66a7748d
JB
248 return transitionAllowed
249}
041365be 250
41f3983a
JB
251export const buildMeterValue = (
252 chargingStation: ChargingStation,
253 connectorId: number,
254 transactionId: number,
255 interval: number,
66a7748d 256 debug = false
41f3983a 257): MeterValue => {
66a7748d
JB
258 const connector = chargingStation.getConnectorStatus(connectorId)
259 let meterValue: MeterValue
260 let socSampledValueTemplate: SampledValueTemplate | undefined
261 let voltageSampledValueTemplate: SampledValueTemplate | undefined
262 let powerSampledValueTemplate: SampledValueTemplate | undefined
263 let powerPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {}
264 let currentSampledValueTemplate: SampledValueTemplate | undefined
265 let currentPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {}
266 let energySampledValueTemplate: SampledValueTemplate | undefined
41f3983a
JB
267 switch (chargingStation.stationInfo?.ocppVersion) {
268 case OCPPVersion.VERSION_16:
269 meterValue = {
270 timestamp: new Date(),
66a7748d
JB
271 sampledValue: []
272 }
41f3983a
JB
273 // SoC measurand
274 socSampledValueTemplate = getSampledValueTemplate(
275 chargingStation,
276 connectorId,
66a7748d
JB
277 MeterValueMeasurand.STATE_OF_CHARGE
278 )
279 if (socSampledValueTemplate != null) {
280 const socMaximumValue = 100
281 const socMinimumValue = socSampledValueTemplate.minimumValue ?? 0
41f3983a
JB
282 const socSampledValueTemplateValue = isNotEmptyString(socSampledValueTemplate.value)
283 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
284 parseInt(socSampledValueTemplate.value),
285 socSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT
286 )
287 : getRandomInteger(socMaximumValue, socMinimumValue)
41f3983a 288 meterValue.sampledValue.push(
66a7748d
JB
289 buildSampledValue(socSampledValueTemplate, socSampledValueTemplateValue)
290 )
291 const sampledValuesIndex = meterValue.sampledValue.length - 1
41f3983a
JB
292 if (
293 convertToInt(meterValue.sampledValue[sampledValuesIndex].value) > socMaximumValue ||
294 convertToInt(meterValue.sampledValue[sampledValuesIndex].value) < socMinimumValue ||
295 debug
296 ) {
297 logger.error(
298 `${chargingStation.logPrefix()} MeterValues measurand ${
299 meterValue.sampledValue[sampledValuesIndex].measurand ??
300 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
301 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${socMinimumValue}/${
302 meterValue.sampledValue[sampledValuesIndex].value
66a7748d
JB
303 }/${socMaximumValue}`
304 )
41f3983a
JB
305 }
306 }
307 // Voltage measurand
308 voltageSampledValueTemplate = getSampledValueTemplate(
309 chargingStation,
310 connectorId,
66a7748d
JB
311 MeterValueMeasurand.VOLTAGE
312 )
313 if (voltageSampledValueTemplate != null) {
41f3983a
JB
314 const voltageSampledValueTemplateValue = isNotEmptyString(voltageSampledValueTemplate.value)
315 ? parseInt(voltageSampledValueTemplate.value)
66a7748d
JB
316 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
317 chargingStation.stationInfo.voltageOut!
41f3983a 318 const fluctuationPercent =
66a7748d 319 voltageSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT
41f3983a
JB
320 const voltageMeasurandValue = getRandomFloatFluctuatedRounded(
321 voltageSampledValueTemplateValue,
66a7748d
JB
322 fluctuationPercent
323 )
41f3983a
JB
324 if (
325 chargingStation.getNumberOfPhases() !== 3 ||
326 (chargingStation.getNumberOfPhases() === 3 &&
5199f9fd 327 chargingStation.stationInfo.mainVoltageMeterValues === true)
41f3983a
JB
328 ) {
329 meterValue.sampledValue.push(
66a7748d
JB
330 buildSampledValue(voltageSampledValueTemplate, voltageMeasurandValue)
331 )
41f3983a
JB
332 }
333 for (
334 let phase = 1;
335 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
336 phase++
337 ) {
66a7748d 338 const phaseLineToNeutralValue = `L${phase}-N`
41f3983a
JB
339 const voltagePhaseLineToNeutralSampledValueTemplate = getSampledValueTemplate(
340 chargingStation,
341 connectorId,
342 MeterValueMeasurand.VOLTAGE,
66a7748d
JB
343 phaseLineToNeutralValue as MeterValuePhase
344 )
345 let voltagePhaseLineToNeutralMeasurandValue: number | undefined
346 if (voltagePhaseLineToNeutralSampledValueTemplate != null) {
41f3983a 347 const voltagePhaseLineToNeutralSampledValueTemplateValue = isNotEmptyString(
66a7748d 348 voltagePhaseLineToNeutralSampledValueTemplate.value
41f3983a
JB
349 )
350 ? parseInt(voltagePhaseLineToNeutralSampledValueTemplate.value)
66a7748d
JB
351 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
352 chargingStation.stationInfo.voltageOut!
41f3983a
JB
353 const fluctuationPhaseToNeutralPercent =
354 voltagePhaseLineToNeutralSampledValueTemplate.fluctuationPercent ??
66a7748d 355 Constants.DEFAULT_FLUCTUATION_PERCENT
41f3983a
JB
356 voltagePhaseLineToNeutralMeasurandValue = getRandomFloatFluctuatedRounded(
357 voltagePhaseLineToNeutralSampledValueTemplateValue,
66a7748d
JB
358 fluctuationPhaseToNeutralPercent
359 )
41f3983a
JB
360 }
361 meterValue.sampledValue.push(
362 buildSampledValue(
363 voltagePhaseLineToNeutralSampledValueTemplate ?? voltageSampledValueTemplate,
364 voltagePhaseLineToNeutralMeasurandValue ?? voltageMeasurandValue,
365 undefined,
66a7748d
JB
366 phaseLineToNeutralValue as MeterValuePhase
367 )
368 )
5199f9fd 369 if (chargingStation.stationInfo.phaseLineToLineVoltageMeterValues === true) {
41f3983a
JB
370 const phaseLineToLineValue = `L${phase}-L${
371 (phase + 1) % chargingStation.getNumberOfPhases() !== 0
372 ? (phase + 1) % chargingStation.getNumberOfPhases()
373 : chargingStation.getNumberOfPhases()
66a7748d 374 }`
41f3983a
JB
375 const voltagePhaseLineToLineValueRounded = roundTo(
376 Math.sqrt(chargingStation.getNumberOfPhases()) *
66a7748d 377 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
41f3983a 378 chargingStation.stationInfo.voltageOut!,
66a7748d
JB
379 2
380 )
41f3983a
JB
381 const voltagePhaseLineToLineSampledValueTemplate = getSampledValueTemplate(
382 chargingStation,
383 connectorId,
384 MeterValueMeasurand.VOLTAGE,
66a7748d
JB
385 phaseLineToLineValue as MeterValuePhase
386 )
387 let voltagePhaseLineToLineMeasurandValue: number | undefined
388 if (voltagePhaseLineToLineSampledValueTemplate != null) {
41f3983a 389 const voltagePhaseLineToLineSampledValueTemplateValue = isNotEmptyString(
66a7748d 390 voltagePhaseLineToLineSampledValueTemplate.value
41f3983a
JB
391 )
392 ? parseInt(voltagePhaseLineToLineSampledValueTemplate.value)
66a7748d 393 : voltagePhaseLineToLineValueRounded
41f3983a
JB
394 const fluctuationPhaseLineToLinePercent =
395 voltagePhaseLineToLineSampledValueTemplate.fluctuationPercent ??
66a7748d 396 Constants.DEFAULT_FLUCTUATION_PERCENT
41f3983a
JB
397 voltagePhaseLineToLineMeasurandValue = getRandomFloatFluctuatedRounded(
398 voltagePhaseLineToLineSampledValueTemplateValue,
66a7748d
JB
399 fluctuationPhaseLineToLinePercent
400 )
41f3983a
JB
401 }
402 const defaultVoltagePhaseLineToLineMeasurandValue = getRandomFloatFluctuatedRounded(
403 voltagePhaseLineToLineValueRounded,
66a7748d
JB
404 fluctuationPercent
405 )
41f3983a
JB
406 meterValue.sampledValue.push(
407 buildSampledValue(
408 voltagePhaseLineToLineSampledValueTemplate ?? voltageSampledValueTemplate,
409 voltagePhaseLineToLineMeasurandValue ?? defaultVoltagePhaseLineToLineMeasurandValue,
410 undefined,
66a7748d
JB
411 phaseLineToLineValue as MeterValuePhase
412 )
413 )
41f3983a
JB
414 }
415 }
416 }
417 // Power.Active.Import measurand
418 powerSampledValueTemplate = getSampledValueTemplate(
419 chargingStation,
420 connectorId,
66a7748d
JB
421 MeterValueMeasurand.POWER_ACTIVE_IMPORT
422 )
41f3983a
JB
423 if (chargingStation.getNumberOfPhases() === 3) {
424 powerPerPhaseSampledValueTemplates = {
425 L1: getSampledValueTemplate(
426 chargingStation,
427 connectorId,
428 MeterValueMeasurand.POWER_ACTIVE_IMPORT,
66a7748d 429 MeterValuePhase.L1_N
41f3983a
JB
430 ),
431 L2: getSampledValueTemplate(
432 chargingStation,
433 connectorId,
434 MeterValueMeasurand.POWER_ACTIVE_IMPORT,
66a7748d 435 MeterValuePhase.L2_N
41f3983a
JB
436 ),
437 L3: getSampledValueTemplate(
438 chargingStation,
439 connectorId,
440 MeterValueMeasurand.POWER_ACTIVE_IMPORT,
66a7748d
JB
441 MeterValuePhase.L3_N
442 )
443 }
41f3983a 444 }
66a7748d
JB
445 if (powerSampledValueTemplate != null) {
446 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5199f9fd 447 checkMeasurandPowerDivider(chargingStation, powerSampledValueTemplate.measurand)
41f3983a
JB
448 const errMsg = `MeterValues measurand ${
449 powerSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
5199f9fd 450 }: Unknown ${chargingStation.stationInfo.currentOutType} currentOutType in template file ${
41f3983a
JB
451 chargingStation.templateFile
452 }, cannot calculate ${
453 powerSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
66a7748d
JB
454 } measurand value`
455 // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
456 const powerMeasurandValues: MeasurandValues = {} as MeasurandValues
5199f9fd 457 const unitDivider = powerSampledValueTemplate.unit === MeterValueUnit.KILO_WATT ? 1000 : 1
41f3983a 458 const connectorMaximumAvailablePower =
66a7748d
JB
459 chargingStation.getConnectorMaximumAvailablePower(connectorId)
460 const connectorMaximumPower = Math.round(connectorMaximumAvailablePower)
41f3983a 461 const connectorMaximumPowerPerPhase = Math.round(
66a7748d
JB
462 connectorMaximumAvailablePower / chargingStation.getNumberOfPhases()
463 )
464 const connectorMinimumPower = Math.round(powerSampledValueTemplate.minimumValue ?? 0)
41f3983a 465 const connectorMinimumPowerPerPhase = Math.round(
66a7748d
JB
466 connectorMinimumPower / chargingStation.getNumberOfPhases()
467 )
5199f9fd 468 switch (chargingStation.stationInfo.currentOutType) {
41f3983a
JB
469 case CurrentType.AC:
470 if (chargingStation.getNumberOfPhases() === 3) {
471 const defaultFluctuatedPowerPerPhase = isNotEmptyString(
66a7748d 472 powerSampledValueTemplate.value
41f3983a
JB
473 )
474 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
475 getLimitFromSampledValueTemplateCustomValue(
476 powerSampledValueTemplate.value,
477 connectorMaximumPower / unitDivider,
478 connectorMinimumPower / unitDivider,
479 {
480 limitationEnabled:
5199f9fd 481 chargingStation.stationInfo.customValueLimitationMeterValues,
66a7748d
JB
482 fallbackValue: connectorMinimumPower / unitDivider
483 }
484 ) / chargingStation.getNumberOfPhases(),
485 powerSampledValueTemplate.fluctuationPercent ??
486 Constants.DEFAULT_FLUCTUATION_PERCENT
487 )
488 : undefined
41f3983a 489 const phase1FluctuatedValue = isNotEmptyString(
66a7748d 490 powerPerPhaseSampledValueTemplates.L1?.value
41f3983a
JB
491 )
492 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
493 getLimitFromSampledValueTemplateCustomValue(
494 powerPerPhaseSampledValueTemplates.L1?.value,
495 connectorMaximumPowerPerPhase / unitDivider,
496 connectorMinimumPowerPerPhase / unitDivider,
497 {
498 limitationEnabled:
5199f9fd 499 chargingStation.stationInfo.customValueLimitationMeterValues,
66a7748d
JB
500 fallbackValue: connectorMinimumPowerPerPhase / unitDivider
501 }
502 ),
503 powerPerPhaseSampledValueTemplates.L1?.fluctuationPercent ??
504 Constants.DEFAULT_FLUCTUATION_PERCENT
505 )
506 : undefined
41f3983a 507 const phase2FluctuatedValue = isNotEmptyString(
66a7748d 508 powerPerPhaseSampledValueTemplates.L2?.value
41f3983a
JB
509 )
510 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
511 getLimitFromSampledValueTemplateCustomValue(
512 powerPerPhaseSampledValueTemplates.L2?.value,
513 connectorMaximumPowerPerPhase / unitDivider,
514 connectorMinimumPowerPerPhase / unitDivider,
515 {
516 limitationEnabled:
5199f9fd 517 chargingStation.stationInfo.customValueLimitationMeterValues,
66a7748d
JB
518 fallbackValue: connectorMinimumPowerPerPhase / unitDivider
519 }
520 ),
521 powerPerPhaseSampledValueTemplates.L2?.fluctuationPercent ??
522 Constants.DEFAULT_FLUCTUATION_PERCENT
523 )
524 : undefined
41f3983a 525 const phase3FluctuatedValue = isNotEmptyString(
66a7748d 526 powerPerPhaseSampledValueTemplates.L3?.value
41f3983a
JB
527 )
528 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
529 getLimitFromSampledValueTemplateCustomValue(
530 powerPerPhaseSampledValueTemplates.L3?.value,
531 connectorMaximumPowerPerPhase / unitDivider,
532 connectorMinimumPowerPerPhase / unitDivider,
533 {
534 limitationEnabled:
5199f9fd 535 chargingStation.stationInfo.customValueLimitationMeterValues,
66a7748d
JB
536 fallbackValue: connectorMinimumPowerPerPhase / unitDivider
537 }
538 ),
539 powerPerPhaseSampledValueTemplates.L3?.fluctuationPercent ??
540 Constants.DEFAULT_FLUCTUATION_PERCENT
541 )
542 : undefined
41f3983a
JB
543 powerMeasurandValues.L1 =
544 phase1FluctuatedValue ??
545 defaultFluctuatedPowerPerPhase ??
546 getRandomFloatRounded(
547 connectorMaximumPowerPerPhase / unitDivider,
66a7748d
JB
548 connectorMinimumPowerPerPhase / unitDivider
549 )
41f3983a
JB
550 powerMeasurandValues.L2 =
551 phase2FluctuatedValue ??
552 defaultFluctuatedPowerPerPhase ??
553 getRandomFloatRounded(
554 connectorMaximumPowerPerPhase / unitDivider,
66a7748d
JB
555 connectorMinimumPowerPerPhase / unitDivider
556 )
41f3983a
JB
557 powerMeasurandValues.L3 =
558 phase3FluctuatedValue ??
559 defaultFluctuatedPowerPerPhase ??
560 getRandomFloatRounded(
561 connectorMaximumPowerPerPhase / unitDivider,
66a7748d
JB
562 connectorMinimumPowerPerPhase / unitDivider
563 )
41f3983a
JB
564 } else {
565 powerMeasurandValues.L1 = isNotEmptyString(powerSampledValueTemplate.value)
566 ? getRandomFloatFluctuatedRounded(
41f3983a
JB
567 getLimitFromSampledValueTemplateCustomValue(
568 powerSampledValueTemplate.value,
569 connectorMaximumPower / unitDivider,
570 connectorMinimumPower / unitDivider,
571 {
572 limitationEnabled:
5199f9fd 573 chargingStation.stationInfo.customValueLimitationMeterValues,
66a7748d
JB
574 fallbackValue: connectorMinimumPower / unitDivider
575 }
41f3983a
JB
576 ),
577 powerSampledValueTemplate.fluctuationPercent ??
66a7748d 578 Constants.DEFAULT_FLUCTUATION_PERCENT
41f3983a 579 )
66a7748d
JB
580 : getRandomFloatRounded(
581 connectorMaximumPower / unitDivider,
582 connectorMinimumPower / unitDivider
583 )
584 powerMeasurandValues.L2 = 0
585 powerMeasurandValues.L3 = 0
586 }
587 powerMeasurandValues.allPhases = roundTo(
588 powerMeasurandValues.L1 + powerMeasurandValues.L2 + powerMeasurandValues.L3,
589 2
590 )
591 break
592 case CurrentType.DC:
593 powerMeasurandValues.allPhases = isNotEmptyString(powerSampledValueTemplate.value)
594 ? getRandomFloatFluctuatedRounded(
595 getLimitFromSampledValueTemplateCustomValue(
596 powerSampledValueTemplate.value,
41f3983a
JB
597 connectorMaximumPower / unitDivider,
598 connectorMinimumPower / unitDivider,
66a7748d
JB
599 {
600 limitationEnabled:
5199f9fd 601 chargingStation.stationInfo.customValueLimitationMeterValues,
66a7748d
JB
602 fallbackValue: connectorMinimumPower / unitDivider
603 }
604 ),
605 powerSampledValueTemplate.fluctuationPercent ??
606 Constants.DEFAULT_FLUCTUATION_PERCENT
607 )
608 : getRandomFloatRounded(
609 connectorMaximumPower / unitDivider,
610 connectorMinimumPower / unitDivider
611 )
612 break
41f3983a 613 default:
66a7748d
JB
614 logger.error(`${chargingStation.logPrefix()} ${errMsg}`)
615 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES)
41f3983a
JB
616 }
617 meterValue.sampledValue.push(
66a7748d
JB
618 buildSampledValue(powerSampledValueTemplate, powerMeasurandValues.allPhases)
619 )
620 const sampledValuesIndex = meterValue.sampledValue.length - 1
621 const connectorMaximumPowerRounded = roundTo(connectorMaximumPower / unitDivider, 2)
622 const connectorMinimumPowerRounded = roundTo(connectorMinimumPower / unitDivider, 2)
41f3983a
JB
623 if (
624 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
625 connectorMaximumPowerRounded ||
626 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) <
627 connectorMinimumPowerRounded ||
628 debug
629 ) {
630 logger.error(
631 `${chargingStation.logPrefix()} MeterValues measurand ${
632 meterValue.sampledValue[sampledValuesIndex].measurand ??
633 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
634 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumPowerRounded}/${
635 meterValue.sampledValue[sampledValuesIndex].value
66a7748d
JB
636 }/${connectorMaximumPowerRounded}`
637 )
41f3983a
JB
638 }
639 for (
640 let phase = 1;
641 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
642 phase++
643 ) {
66a7748d 644 const phaseValue = `L${phase}-N`
41f3983a
JB
645 meterValue.sampledValue.push(
646 buildSampledValue(
647 powerPerPhaseSampledValueTemplates[
648 `L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates
649 ] ?? powerSampledValueTemplate,
650 powerMeasurandValues[`L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates],
651 undefined,
66a7748d
JB
652 phaseValue as MeterValuePhase
653 )
654 )
655 const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1
41f3983a
JB
656 const connectorMaximumPowerPerPhaseRounded = roundTo(
657 connectorMaximumPowerPerPhase / unitDivider,
66a7748d
JB
658 2
659 )
41f3983a
JB
660 const connectorMinimumPowerPerPhaseRounded = roundTo(
661 connectorMinimumPowerPerPhase / unitDivider,
66a7748d
JB
662 2
663 )
41f3983a
JB
664 if (
665 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
666 connectorMaximumPowerPerPhaseRounded ||
667 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
668 connectorMinimumPowerPerPhaseRounded ||
669 debug
670 ) {
671 logger.error(
672 `${chargingStation.logPrefix()} MeterValues measurand ${
673 meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ??
674 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
675 }: phase ${
676 meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
677 }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumPowerPerPhaseRounded}/${
678 meterValue.sampledValue[sampledValuesPerPhaseIndex].value
66a7748d
JB
679 }/${connectorMaximumPowerPerPhaseRounded}`
680 )
41f3983a
JB
681 }
682 }
683 }
684 // Current.Import measurand
685 currentSampledValueTemplate = getSampledValueTemplate(
686 chargingStation,
687 connectorId,
66a7748d
JB
688 MeterValueMeasurand.CURRENT_IMPORT
689 )
41f3983a
JB
690 if (chargingStation.getNumberOfPhases() === 3) {
691 currentPerPhaseSampledValueTemplates = {
692 L1: getSampledValueTemplate(
693 chargingStation,
694 connectorId,
695 MeterValueMeasurand.CURRENT_IMPORT,
66a7748d 696 MeterValuePhase.L1
41f3983a
JB
697 ),
698 L2: getSampledValueTemplate(
699 chargingStation,
700 connectorId,
701 MeterValueMeasurand.CURRENT_IMPORT,
66a7748d 702 MeterValuePhase.L2
41f3983a
JB
703 ),
704 L3: getSampledValueTemplate(
705 chargingStation,
706 connectorId,
707 MeterValueMeasurand.CURRENT_IMPORT,
66a7748d
JB
708 MeterValuePhase.L3
709 )
710 }
41f3983a 711 }
66a7748d
JB
712 if (currentSampledValueTemplate != null) {
713 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5199f9fd 714 checkMeasurandPowerDivider(chargingStation, currentSampledValueTemplate.measurand)
41f3983a
JB
715 const errMsg = `MeterValues measurand ${
716 currentSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
5199f9fd 717 }: Unknown ${chargingStation.stationInfo.currentOutType} currentOutType in template file ${
41f3983a
JB
718 chargingStation.templateFile
719 }, cannot calculate ${
720 currentSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
66a7748d
JB
721 } measurand value`
722 // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
723 const currentMeasurandValues: MeasurandValues = {} as MeasurandValues
41f3983a 724 const connectorMaximumAvailablePower =
66a7748d
JB
725 chargingStation.getConnectorMaximumAvailablePower(connectorId)
726 const connectorMinimumAmperage = currentSampledValueTemplate.minimumValue ?? 0
727 let connectorMaximumAmperage: number
5199f9fd 728 switch (chargingStation.stationInfo.currentOutType) {
41f3983a
JB
729 case CurrentType.AC:
730 connectorMaximumAmperage = ACElectricUtils.amperagePerPhaseFromPower(
731 chargingStation.getNumberOfPhases(),
732 connectorMaximumAvailablePower,
66a7748d
JB
733 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
734 chargingStation.stationInfo.voltageOut!
735 )
41f3983a
JB
736 if (chargingStation.getNumberOfPhases() === 3) {
737 const defaultFluctuatedAmperagePerPhase = isNotEmptyString(
66a7748d 738 currentSampledValueTemplate.value
41f3983a
JB
739 )
740 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
741 getLimitFromSampledValueTemplateCustomValue(
742 currentSampledValueTemplate.value,
743 connectorMaximumAmperage,
744 connectorMinimumAmperage,
745 {
746 limitationEnabled:
5199f9fd 747 chargingStation.stationInfo.customValueLimitationMeterValues,
66a7748d
JB
748 fallbackValue: connectorMinimumAmperage
749 }
750 ),
751 currentSampledValueTemplate.fluctuationPercent ??
752 Constants.DEFAULT_FLUCTUATION_PERCENT
753 )
754 : undefined
41f3983a 755 const phase1FluctuatedValue = isNotEmptyString(
66a7748d 756 currentPerPhaseSampledValueTemplates.L1?.value
41f3983a
JB
757 )
758 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
759 getLimitFromSampledValueTemplateCustomValue(
760 currentPerPhaseSampledValueTemplates.L1?.value,
761 connectorMaximumAmperage,
762 connectorMinimumAmperage,
763 {
764 limitationEnabled:
5199f9fd 765 chargingStation.stationInfo.customValueLimitationMeterValues,
66a7748d
JB
766 fallbackValue: connectorMinimumAmperage
767 }
768 ),
769 currentPerPhaseSampledValueTemplates.L1?.fluctuationPercent ??
770 Constants.DEFAULT_FLUCTUATION_PERCENT
771 )
772 : undefined
41f3983a 773 const phase2FluctuatedValue = isNotEmptyString(
66a7748d 774 currentPerPhaseSampledValueTemplates.L2?.value
41f3983a
JB
775 )
776 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
777 getLimitFromSampledValueTemplateCustomValue(
778 currentPerPhaseSampledValueTemplates.L2?.value,
779 connectorMaximumAmperage,
780 connectorMinimumAmperage,
781 {
782 limitationEnabled:
5199f9fd 783 chargingStation.stationInfo.customValueLimitationMeterValues,
66a7748d
JB
784 fallbackValue: connectorMinimumAmperage
785 }
786 ),
787 currentPerPhaseSampledValueTemplates.L2?.fluctuationPercent ??
788 Constants.DEFAULT_FLUCTUATION_PERCENT
789 )
790 : undefined
41f3983a 791 const phase3FluctuatedValue = isNotEmptyString(
66a7748d 792 currentPerPhaseSampledValueTemplates.L3?.value
41f3983a
JB
793 )
794 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
795 getLimitFromSampledValueTemplateCustomValue(
796 currentPerPhaseSampledValueTemplates.L3?.value,
797 connectorMaximumAmperage,
798 connectorMinimumAmperage,
799 {
800 limitationEnabled:
5199f9fd 801 chargingStation.stationInfo.customValueLimitationMeterValues,
66a7748d
JB
802 fallbackValue: connectorMinimumAmperage
803 }
804 ),
805 currentPerPhaseSampledValueTemplates.L3?.fluctuationPercent ??
806 Constants.DEFAULT_FLUCTUATION_PERCENT
807 )
808 : undefined
41f3983a
JB
809 currentMeasurandValues.L1 =
810 phase1FluctuatedValue ??
811 defaultFluctuatedAmperagePerPhase ??
66a7748d 812 getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage)
41f3983a
JB
813 currentMeasurandValues.L2 =
814 phase2FluctuatedValue ??
815 defaultFluctuatedAmperagePerPhase ??
66a7748d 816 getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage)
41f3983a
JB
817 currentMeasurandValues.L3 =
818 phase3FluctuatedValue ??
819 defaultFluctuatedAmperagePerPhase ??
66a7748d 820 getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage)
41f3983a
JB
821 } else {
822 currentMeasurandValues.L1 = isNotEmptyString(currentSampledValueTemplate.value)
823 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
824 getLimitFromSampledValueTemplateCustomValue(
825 currentSampledValueTemplate.value,
826 connectorMaximumAmperage,
827 connectorMinimumAmperage,
828 {
829 limitationEnabled:
5199f9fd 830 chargingStation.stationInfo.customValueLimitationMeterValues,
66a7748d
JB
831 fallbackValue: connectorMinimumAmperage
832 }
833 ),
834 currentSampledValueTemplate.fluctuationPercent ??
835 Constants.DEFAULT_FLUCTUATION_PERCENT
836 )
837 : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage)
838 currentMeasurandValues.L2 = 0
839 currentMeasurandValues.L3 = 0
41f3983a
JB
840 }
841 currentMeasurandValues.allPhases = roundTo(
842 (currentMeasurandValues.L1 + currentMeasurandValues.L2 + currentMeasurandValues.L3) /
843 chargingStation.getNumberOfPhases(),
66a7748d
JB
844 2
845 )
846 break
41f3983a
JB
847 case CurrentType.DC:
848 connectorMaximumAmperage = DCElectricUtils.amperage(
849 connectorMaximumAvailablePower,
66a7748d
JB
850 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
851 chargingStation.stationInfo.voltageOut!
852 )
41f3983a
JB
853 currentMeasurandValues.allPhases = isNotEmptyString(currentSampledValueTemplate.value)
854 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
855 getLimitFromSampledValueTemplateCustomValue(
856 currentSampledValueTemplate.value,
857 connectorMaximumAmperage,
858 connectorMinimumAmperage,
859 {
860 limitationEnabled:
5199f9fd 861 chargingStation.stationInfo.customValueLimitationMeterValues,
66a7748d
JB
862 fallbackValue: connectorMinimumAmperage
863 }
864 ),
865 currentSampledValueTemplate.fluctuationPercent ??
866 Constants.DEFAULT_FLUCTUATION_PERCENT
867 )
868 : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage)
869 break
41f3983a 870 default:
66a7748d
JB
871 logger.error(`${chargingStation.logPrefix()} ${errMsg}`)
872 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES)
41f3983a
JB
873 }
874 meterValue.sampledValue.push(
66a7748d
JB
875 buildSampledValue(currentSampledValueTemplate, currentMeasurandValues.allPhases)
876 )
877 const sampledValuesIndex = meterValue.sampledValue.length - 1
41f3983a
JB
878 if (
879 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
880 connectorMaximumAmperage ||
881 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) <
882 connectorMinimumAmperage ||
883 debug
884 ) {
885 logger.error(
886 `${chargingStation.logPrefix()} MeterValues measurand ${
887 meterValue.sampledValue[sampledValuesIndex].measurand ??
888 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
889 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${
890 meterValue.sampledValue[sampledValuesIndex].value
66a7748d
JB
891 }/${connectorMaximumAmperage}`
892 )
41f3983a
JB
893 }
894 for (
895 let phase = 1;
896 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
897 phase++
898 ) {
66a7748d 899 const phaseValue = `L${phase}`
41f3983a
JB
900 meterValue.sampledValue.push(
901 buildSampledValue(
902 currentPerPhaseSampledValueTemplates[
903 phaseValue as keyof MeasurandPerPhaseSampledValueTemplates
904 ] ?? currentSampledValueTemplate,
905 currentMeasurandValues[phaseValue as keyof MeasurandPerPhaseSampledValueTemplates],
906 undefined,
66a7748d
JB
907 phaseValue as MeterValuePhase
908 )
909 )
910 const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1
41f3983a
JB
911 if (
912 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
913 connectorMaximumAmperage ||
914 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
915 connectorMinimumAmperage ||
916 debug
917 ) {
918 logger.error(
919 `${chargingStation.logPrefix()} MeterValues measurand ${
920 meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ??
921 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
922 }: phase ${
923 meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
924 }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${
925 meterValue.sampledValue[sampledValuesPerPhaseIndex].value
66a7748d
JB
926 }/${connectorMaximumAmperage}`
927 )
41f3983a
JB
928 }
929 }
930 }
931 // Energy.Active.Import.Register measurand (default)
66a7748d
JB
932 energySampledValueTemplate = getSampledValueTemplate(chargingStation, connectorId)
933 if (energySampledValueTemplate != null) {
934 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5199f9fd 935 checkMeasurandPowerDivider(chargingStation, energySampledValueTemplate.measurand)
41f3983a 936 const unitDivider =
5199f9fd 937 energySampledValueTemplate.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1
41f3983a 938 const connectorMaximumAvailablePower =
66a7748d 939 chargingStation.getConnectorMaximumAvailablePower(connectorId)
41f3983a
JB
940 const connectorMaximumEnergyRounded = roundTo(
941 (connectorMaximumAvailablePower * interval) / (3600 * 1000),
66a7748d
JB
942 2
943 )
41f3983a
JB
944 const connectorMinimumEnergyRounded = roundTo(
945 energySampledValueTemplate.minimumValue ?? 0,
66a7748d
JB
946 2
947 )
41f3983a
JB
948 const energyValueRounded = isNotEmptyString(energySampledValueTemplate.value)
949 ? getRandomFloatFluctuatedRounded(
66a7748d
JB
950 getLimitFromSampledValueTemplateCustomValue(
951 energySampledValueTemplate.value,
952 connectorMaximumEnergyRounded,
953 connectorMinimumEnergyRounded,
954 {
5199f9fd 955 limitationEnabled: chargingStation.stationInfo.customValueLimitationMeterValues,
66a7748d
JB
956 fallbackValue: connectorMinimumEnergyRounded,
957 unitMultiplier: unitDivider
958 }
959 ),
960 energySampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT
961 )
962 : getRandomFloatRounded(connectorMaximumEnergyRounded, connectorMinimumEnergyRounded)
41f3983a 963 // Persist previous value on connector
66a7748d 964 if (connector != null) {
41f3983a 965 if (
be9f397b
JB
966 connector.energyActiveImportRegisterValue != null &&
967 connector.energyActiveImportRegisterValue >= 0 &&
968 connector.transactionEnergyActiveImportRegisterValue != null &&
969 connector.transactionEnergyActiveImportRegisterValue >= 0
41f3983a 970 ) {
be9f397b
JB
971 connector.energyActiveImportRegisterValue += energyValueRounded
972 connector.transactionEnergyActiveImportRegisterValue += energyValueRounded
41f3983a 973 } else {
66a7748d
JB
974 connector.energyActiveImportRegisterValue = 0
975 connector.transactionEnergyActiveImportRegisterValue = 0
41f3983a
JB
976 }
977 }
978 meterValue.sampledValue.push(
979 buildSampledValue(
980 energySampledValueTemplate,
981 roundTo(
982 chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) /
983 unitDivider,
66a7748d
JB
984 2
985 )
986 )
987 )
988 const sampledValuesIndex = meterValue.sampledValue.length - 1
41f3983a
JB
989 if (
990 energyValueRounded > connectorMaximumEnergyRounded ||
991 energyValueRounded < connectorMinimumEnergyRounded ||
992 debug
993 ) {
994 logger.error(
995 `${chargingStation.logPrefix()} MeterValues measurand ${
996 meterValue.sampledValue[sampledValuesIndex].measurand ??
997 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
66a7748d
JB
998 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumEnergyRounded}/${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${interval}ms`
999 )
41f3983a
JB
1000 }
1001 }
66a7748d 1002 return meterValue
41f3983a
JB
1003 case OCPPVersion.VERSION_20:
1004 case OCPPVersion.VERSION_201:
1005 default:
1006 throw new BaseError(
66a7748d
JB
1007 `Cannot build meterValue: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`
1008 )
41f3983a 1009 }
66a7748d 1010}
41f3983a
JB
1011
1012export const buildTransactionEndMeterValue = (
1013 chargingStation: ChargingStation,
1014 connectorId: number,
5199f9fd 1015 meterStop: number | undefined
41f3983a 1016): MeterValue => {
66a7748d
JB
1017 let meterValue: MeterValue
1018 let sampledValueTemplate: SampledValueTemplate | undefined
1019 let unitDivider: number
41f3983a
JB
1020 switch (chargingStation.stationInfo?.ocppVersion) {
1021 case OCPPVersion.VERSION_16:
1022 meterValue = {
1023 timestamp: new Date(),
66a7748d
JB
1024 sampledValue: []
1025 }
41f3983a 1026 // Energy.Active.Import.Register measurand (default)
66a7748d
JB
1027 sampledValueTemplate = getSampledValueTemplate(chargingStation, connectorId)
1028 unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1
41f3983a
JB
1029 meterValue.sampledValue.push(
1030 buildSampledValue(
66a7748d 1031 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
41f3983a
JB
1032 sampledValueTemplate!,
1033 roundTo((meterStop ?? 0) / unitDivider, 4),
66a7748d
JB
1034 MeterValueContext.TRANSACTION_END
1035 )
1036 )
1037 return meterValue
41f3983a
JB
1038 case OCPPVersion.VERSION_20:
1039 case OCPPVersion.VERSION_201:
1040 default:
1041 throw new BaseError(
66a7748d
JB
1042 `Cannot build meterValue: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`
1043 )
41f3983a 1044 }
66a7748d 1045}
41f3983a
JB
1046
1047const checkMeasurandPowerDivider = (
1048 chargingStation: ChargingStation,
5199f9fd 1049 measurandType: MeterValueMeasurand | undefined
41f3983a 1050): void => {
300418e9 1051 if (chargingStation.powerDivider == null) {
41f3983a
JB
1052 const errMsg = `MeterValues measurand ${
1053 measurandType ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
66a7748d
JB
1054 }: powerDivider is undefined`
1055 logger.error(`${chargingStation.logPrefix()} ${errMsg}`)
1056 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES)
5199f9fd 1057 } else if (chargingStation.powerDivider <= 0) {
41f3983a
JB
1058 const errMsg = `MeterValues measurand ${
1059 measurandType ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
66a7748d
JB
1060 }: powerDivider have zero or below value ${chargingStation.powerDivider}`
1061 logger.error(`${chargingStation.logPrefix()} ${errMsg}`)
1062 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES)
41f3983a 1063 }
66a7748d 1064}
41f3983a
JB
1065
1066const getLimitFromSampledValueTemplateCustomValue = (
1067 value: string | undefined,
1068 maxLimit: number,
1069 minLimit: number,
66a7748d 1070 options?: { limitationEnabled?: boolean, fallbackValue?: number, unitMultiplier?: number }
41f3983a
JB
1071): number => {
1072 options = {
1073 ...{
1074 limitationEnabled: false,
1075 unitMultiplier: 1,
66a7748d 1076 fallbackValue: 0
41f3983a 1077 },
66a7748d
JB
1078 ...options
1079 }
1080 const parsedValue = parseInt(value ?? '')
5199f9fd 1081 if (options.limitationEnabled === true) {
41f3983a 1082 return max(
66a7748d 1083 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
41f3983a 1084 min((!isNaN(parsedValue) ? parsedValue : Infinity) * options.unitMultiplier!, maxLimit),
66a7748d
JB
1085 minLimit
1086 )
41f3983a 1087 }
66a7748d
JB
1088 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1089 return (!isNaN(parsedValue) ? parsedValue : options.fallbackValue!) * options.unitMultiplier!
1090}
41f3983a
JB
1091
1092const getSampledValueTemplate = (
1093 chargingStation: ChargingStation,
1094 connectorId: number,
1095 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
66a7748d 1096 phase?: MeterValuePhase
41f3983a 1097): SampledValueTemplate | undefined => {
66a7748d
JB
1098 const onPhaseStr = phase != null ? `on phase ${phase} ` : ''
1099 if (!OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(measurand)) {
41f3983a 1100 logger.warn(
66a7748d
JB
1101 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
1102 )
1103 return
41f3983a
JB
1104 }
1105 if (
1106 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
1107 getConfigurationKey(
1108 chargingStation,
66a7748d 1109 StandardParametersKey.MeterValuesSampledData
41f3983a
JB
1110 )?.value?.includes(measurand) === false
1111 ) {
1112 logger.debug(
1113 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
1114 StandardParametersKey.MeterValuesSampledData
66a7748d
JB
1115 }' OCPP parameter`
1116 )
1117 return
41f3983a
JB
1118 }
1119 const sampledValueTemplates: SampledValueTemplate[] =
66a7748d
JB
1120 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1121 chargingStation.getConnectorStatus(connectorId)!.MeterValues
41f3983a
JB
1122 for (
1123 let index = 0;
66a7748d 1124 isNotEmptyArray(sampledValueTemplates) && index < sampledValueTemplates.length;
41f3983a
JB
1125 index++
1126 ) {
1127 if (
66a7748d
JB
1128 !OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(
1129 sampledValueTemplates[index]?.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1130 )
41f3983a
JB
1131 ) {
1132 logger.warn(
66a7748d
JB
1133 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
1134 )
41f3983a 1135 } else if (
66a7748d 1136 phase != null &&
41f3983a
JB
1137 sampledValueTemplates[index]?.phase === phase &&
1138 sampledValueTemplates[index]?.measurand === measurand &&
1139 getConfigurationKey(
1140 chargingStation,
66a7748d 1141 StandardParametersKey.MeterValuesSampledData
41f3983a
JB
1142 )?.value?.includes(measurand) === true
1143 ) {
66a7748d 1144 return sampledValueTemplates[index]
41f3983a 1145 } else if (
66a7748d
JB
1146 phase == null &&
1147 sampledValueTemplates[index]?.phase == null &&
41f3983a
JB
1148 sampledValueTemplates[index]?.measurand === measurand &&
1149 getConfigurationKey(
1150 chargingStation,
66a7748d 1151 StandardParametersKey.MeterValuesSampledData
41f3983a
JB
1152 )?.value?.includes(measurand) === true
1153 ) {
66a7748d 1154 return sampledValueTemplates[index]
41f3983a
JB
1155 } else if (
1156 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
66a7748d 1157 (sampledValueTemplates[index]?.measurand == null ||
41f3983a
JB
1158 sampledValueTemplates[index]?.measurand === measurand)
1159 ) {
66a7748d 1160 return sampledValueTemplates[index]
41f3983a
JB
1161 }
1162 }
1163 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
66a7748d
JB
1164 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`
1165 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`)
1166 throw new BaseError(errorMsg)
41f3983a
JB
1167 }
1168 logger.debug(
66a7748d
JB
1169 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
1170 )
1171}
41f3983a
JB
1172
1173const buildSampledValue = (
1174 sampledValueTemplate: SampledValueTemplate,
1175 value: number,
1176 context?: MeterValueContext,
66a7748d 1177 phase?: MeterValuePhase
41f3983a 1178): SampledValue => {
5199f9fd 1179 const sampledValueContext = context ?? sampledValueTemplate.context
41f3983a 1180 const sampledValueLocation =
66a7748d 1181 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5199f9fd
JB
1182 sampledValueTemplate.location ?? getMeasurandDefaultLocation(sampledValueTemplate.measurand!)
1183 const sampledValuePhase = phase ?? sampledValueTemplate.phase
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 1192 ...(sampledValueLocation != null && { location: sampledValueLocation }),
5199f9fd 1193 ...{ value: value.toString() },
be9f397b 1194 ...(sampledValuePhase != null && { phase: sampledValuePhase })
5199f9fd 1195 } satisfies SampledValue
66a7748d 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 ) {
5199f9fd 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 1292 isIncomingRequestCommand &&
5199f9fd 1293 chargingStation.stationInfo?.commandsSupport?.incomingCommands[command] != null
ed3d2808 1294 ) {
5199f9fd 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 ) {
5199f9fd 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()
5199f9fd
JB
1340 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-condition
1341 } else if (typeof obj![key] === 'object' && obj![key] !== null) {
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}