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