UNPKG

@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.

206 lines 9.19 kB
import { MeasurandEnum, PhaseEnum, ReadingContextEnum, } from '../interfaces/dto/types/enums.js'; export class MeterValueUtils { static validContexts = new Set([ ReadingContextEnum['Transaction.Begin'], ReadingContextEnum['Sample.Periodic'], ReadingContextEnum['Transaction.End'], ]); /** * Calculate the total Kwh * * @param {array} meterValues - meterValues of a transaction. * @param {number} currentTotal - the current total Kwh to add to interval values, if needed. * @param {number} meterStart - the starting Kwh value at the beginning of the transaction, if available. * @return {number} total Kwh based on the best available energy measurement. */ static getTotalKwh(meterValues, currentTotal, meterStart) { const filteredValues = this.filterValidMeterValues(meterValues); if (filteredValues.length === 0) { return 0; } const registerMap = this.getRegisterValuesMap(filteredValues); if (registerMap.size > 0) { const sorted = this.getSortedKwhByTimestampAscending(registerMap); if (meterStart === undefined) { return sorted[sorted.length - 1] - sorted[0]; } return sorted[sorted.length - 1] - meterStart; } const intervalMap = this.getIntervalValuesMap(filteredValues); if (intervalMap.size > 0) { const sorted = this.getSortedKwhByTimestampAscending(intervalMap); return sorted.reduce((sum, v) => sum + v, currentTotal); } const netMap = this.getNetValuesMap(filteredValues); if (netMap.size > 0) { const latestTimestamp = Math.max(...Array.from(netMap.keys())); return netMap.get(latestTimestamp); } return 0; } static getMeterStart(meterValues) { const filteredValues = this.filterValidMeterValues(meterValues); if (filteredValues.length === 0) { return null; } const registerMap = this.getRegisterValuesMap(filteredValues); if (registerMap.size > 0) { const sorted = this.getSortedKwhByTimestampAscending(registerMap); return sorted[0]; } return null; } /** * Filter out meter values whose context is not one of the valid reading contexts. * @param meterValues Array of MeterValueType to filter. * @returns Filtered array containing only meter values in Transaction_Begin, Sample_Periodic or Transaction_End contexts. */ 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)); } /** * Extracts Energy.Active.Import.Register measurand values into a timestamp-to-kWh map. * @param meterValues Array of MeterValueType to search for register readings. * @returns Map where each key is the reading timestamp (ms since epoch) and each value is the normalized kWh. */ static getRegisterValuesMap(meterValues) { const valuesMap = new Map(); for (const mv of meterValues) { const ts = Date.parse(mv.timestamp); let val = this.findMeasurandValue(mv.sampledValue, MeasurandEnum['Energy.Active.Import.Register'], false); if (val === null) { val = this.sumPhasedValues(mv.sampledValue, MeasurandEnum['Energy.Active.Import.Register']); } if (val !== null) { valuesMap.set(ts, val); } } return valuesMap; } /** * Extracts Energy.Active.Import.Interval measurand values into a timestamp-to-kWh map. * @param meterValues Array of MeterValueType to search for interval readings. * @returns Map where each key is the reading timestamp (ms since epoch) and each value is the normalized kWh. */ static getIntervalValuesMap(meterValues) { const valuesMap = new Map(); for (const mv of meterValues) { const ts = Date.parse(mv.timestamp); let val = this.findMeasurandValue(mv.sampledValue, MeasurandEnum['Energy.Active.Import.Interval'], false); if (val === null) { val = this.sumPhasedValues(mv.sampledValue, MeasurandEnum['Energy.Active.Import.Interval']); } if (val !== null) { valuesMap.set(ts, val); } } return valuesMap; } /** * Extracts Energy.Active.Net measurand values into a timestamp-to-kWh map. * @param meterValues Array of MeterValueType to search for net readings. * @returns Map where each key is the reading timestamp (ms since epoch) and each value is the normalized kWh. */ static getNetValuesMap(meterValues) { const valuesMap = new Map(); for (const mv of meterValues) { const ts = Date.parse(mv.timestamp); const val = this.findMeasurandValue(mv.sampledValue, MeasurandEnum['Energy.Active.Net'], false); if (val !== null) { valuesMap.set(ts, val); } } 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 || (!sv.measurand && // Default to Energy.Active.Import.Register if measurand is missing measurand === MeasurandEnum['Energy.Active.Import.Register'])) && !phased === !sv.phase); return value ? this.normalizeToKwh(value) : 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 === PhaseEnum.L1 || sv.phase === PhaseEnum.L2 || sv.phase === PhaseEnum.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 === PhaseEnum['L1-N'] || sv.phase === PhaseEnum['L2-N'] || sv.phase === PhaseEnum['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; } /** * Convert a sampled value to kWh, applying unit multipliers. * @param value A SampledValueType entry. * @returns The converted value in kWh, or null if unit is missing. */ static normalizeToKwh(value) { let powerOfTen = value.unitOfMeasure?.multiplier ?? 0; const unit = value.unitOfMeasure?.unit?.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 * 10 ** powerOfTen; } /** * Sort the entries of a timestamp-to-kWh map ascending by timestamp and return the kWh values. * @param valuesMap Map of timestamp (ms since epoch) to kWh. * @returns Array of kWh values sorted by timestamp. */ static getSortedKwhByTimestampAscending(valuesMap) { return Array.from(valuesMap.entries()) .sort((a, b) => a[0] - b[0]) .map(([, v]) => v); } } //# sourceMappingURL=MeterValueUtils.js.map