@citrineos/base
Version:
The base module for OCPP v2.0.1 including all interfaces. This module is not intended to be used directly, but rather as a dependency for other modules.
144 lines • 6.59 kB
JavaScript
import { OCPP2_0_1 } from '../ocpp/model';
export class MeterValueUtils {
/**
* Calculate the total Kwh
*
* @param {array} meterValues - meterValues of a transaction.
* @return {number} total Kwh based on the best available energy measurement.
*/
static getTotalKwh(meterValues) {
const filteredValues = this.filterValidMeterValues(meterValues);
const timestampToKwhMap = this.getTimestampToKwhMap(filteredValues);
const sortedValues = this.getSortedKwhByTimestampAscending(timestampToKwhMap);
return this.calculateTotalKwh(sortedValues);
}
static filterValidMeterValues(meterValues) {
return meterValues.filter((mv) =>
// When missing, context is by default Sample_Periodic by spec
!mv.sampledValue[0].context || this.validContexts.has(mv.sampledValue[0].context));
}
static getTimestampToKwhMap(meterValues) {
const valuesMap = new Map();
for (const meterValue of meterValues) {
const timestamp = Date.parse(meterValue.timestamp);
let energyValue = null;
// Try strategies in order of preference
// 1. Overall Energy.Active.Import.Register
energyValue = this.findMeasurandValue(meterValue.sampledValue, OCPP2_0_1.MeasurandEnumType.Energy_Active_Import_Register, false);
// 2. Energy.Active.Import.Interval
if (energyValue === null) {
energyValue = this.findMeasurandValue(meterValue.sampledValue, OCPP2_0_1.MeasurandEnumType.Energy_Active_Import_Interval, false);
}
// 3. Energy.Active.Net
if (energyValue === null) {
energyValue = this.findMeasurandValue(meterValue.sampledValue, OCPP2_0_1.MeasurandEnumType.Energy_Active_Net, false);
}
// 4. Sum of phased Energy.Active.Import.Register values
if (energyValue === null) {
energyValue = this.sumPhasedValues(meterValue.sampledValue, OCPP2_0_1.MeasurandEnumType.Energy_Active_Import_Register);
}
// 5. Sum of phased Energy.Active.Import.Interval values
if (energyValue === null) {
energyValue = this.sumPhasedValues(meterValue.sampledValue, OCPP2_0_1.MeasurandEnumType.Energy_Active_Import_Interval);
}
// Store the value if we found one
if (energyValue !== null) {
valuesMap.set(timestamp, energyValue);
}
}
return valuesMap;
}
/**
* Find a specific measurand value from sampledValues
* @param sampledValues Array of sampled values
* @param measurand The measurand type to look for
* @param phased Whether to look for phased values (true) or non-phased values (false)
* @returns The normalized value in kWh, or null if not found
*/
static findMeasurandValue(sampledValues, measurand, phased) {
const value = sampledValues.find((sv) => sv.measurand === measurand && !phased === !sv.phase);
if (value) {
return this.normalizeToKwh(value);
}
return null;
}
/**
* Sum phased values for a specific measurand
* @param sampledValues Array of sampled values
* @param measurand The measurand type to sum
* @returns The sum of phase values in kWh, or null if no valid phase values found
*/
static sumPhasedValues(sampledValues, measurand) {
// Find all values for this measurand that have individual phases L1, L2, or L3
const phaseValues = sampledValues.filter((sv) => sv.measurand === measurand &&
sv.phase && // Must have a phase specified
(sv.phase === OCPP2_0_1.PhaseEnumType.L1 ||
sv.phase === OCPP2_0_1.PhaseEnumType.L2 ||
sv.phase === OCPP2_0_1.PhaseEnumType.L3));
// If no phase values found, try phase-to-neutral as a last resort
if (phaseValues.length === 0) {
const phaseNeutralValues = sampledValues.filter((sv) => sv.measurand === measurand &&
sv.phase && // Must have a phase specified
(sv.phase === OCPP2_0_1.PhaseEnumType.L1_N ||
sv.phase === OCPP2_0_1.PhaseEnumType.L2_N ||
sv.phase === OCPP2_0_1.PhaseEnumType.L3_N));
if (phaseNeutralValues.length === 0) {
return null;
}
let sum = 0;
for (const value of phaseNeutralValues) {
const normalizedValue = this.normalizeToKwh(value);
if (normalizedValue !== null) {
sum += normalizedValue;
}
}
return sum;
}
// Sum all the normalized phase values
let sum = 0;
for (const value of phaseValues) {
const normalizedValue = this.normalizeToKwh(value);
if (normalizedValue !== null) {
sum += normalizedValue;
}
}
return sum;
}
static normalizeToKwh(value) {
var _a, _b, _c, _d;
let powerOfTen = (_b = (_a = value.unitOfMeasure) === null || _a === void 0 ? void 0 : _a.multiplier) !== null && _b !== void 0 ? _b : 0;
const unit = (_d = (_c = value.unitOfMeasure) === null || _c === void 0 ? void 0 : _c.unit) === null || _d === void 0 ? void 0 : _d.toUpperCase();
switch (unit) {
case 'KWH':
case 'KVARH': // For reactive energy
case 'KVAH': // For apparent energy
break;
case 'WH':
case 'VARH': // For reactive energy
case 'VAH': // For apparent energy
case undefined:
powerOfTen -= 3;
break;
default:
throw new Error(`Unknown unit for energy measurement: ${unit}`);
}
return value.value * Math.pow(10, powerOfTen);
}
static getSortedKwhByTimestampAscending(valuesMap) {
return Array.from(valuesMap.entries())
.sort((a, b) => a[0] - b[0])
.map((entry) => entry[1]);
}
static calculateTotalKwh(sortedValues) {
if (sortedValues.length < 2) {
return 0;
}
return sortedValues[sortedValues.length - 1] - sortedValues[0];
}
}
MeterValueUtils.validContexts = new Set([
OCPP2_0_1.ReadingContextEnumType.Transaction_Begin,
OCPP2_0_1.ReadingContextEnumType.Sample_Periodic,
OCPP2_0_1.ReadingContextEnumType.Transaction_End,
]);
//# sourceMappingURL=MeterValueUtils.js.map