diff --git a/00_Base/src/ocpp/persistence/namespace.ts b/00_Base/src/ocpp/persistence/namespace.ts index e3ce9fee..d3fc5937 100644 --- a/00_Base/src/ocpp/persistence/namespace.ts +++ b/00_Base/src/ocpp/persistence/namespace.ts @@ -9,6 +9,7 @@ export enum Namespace { BootConfig = 'Boot', + MeterValue = 'MeterValue', } export enum OCPP2_0_1_Namespace { @@ -34,7 +35,6 @@ export enum OCPP2_0_1_Namespace { LocalListAuthorization = 'LocalListAuthorization', LocalListVersion = 'LocalListVersion', Location = 'Location', - MeterValueType = 'MeterValue', MessageInfoType = 'MessageInfo', PasswordType = 'Password', ReserveNowRequest = 'Reservation', diff --git a/01_Data/src/index.ts b/01_Data/src/index.ts index ad1452bf..eddff7a0 100644 --- a/01_Data/src/index.ts +++ b/01_Data/src/index.ts @@ -65,6 +65,7 @@ export { SequelizeTransactionEventRepository, SequelizeVariableMonitoringRepository, SequelizeChargingStationSequenceRepository, + OCPP2_0_1_Mapper } from './layers/sequelize'; // TODO ensure all needed modules are properly exported export { RepositoryStore } from './layers/sequelize/repository/RepositoryStore'; export { CryptoUtils } from './util/CryptoUtils'; diff --git a/01_Data/src/interfaces/repositories.ts b/01_Data/src/interfaces/repositories.ts index 93283f12..9642a4cc 100644 --- a/01_Data/src/interfaces/repositories.ts +++ b/01_Data/src/interfaces/repositories.ts @@ -38,6 +38,7 @@ import { type VariableAttribute, VariableCharacteristics, type VariableMonitoring, + TransactionEvent, } from '../layers/sequelize'; import { type AuthorizationRestrictions, type VariableAttributeQuerystring } from '.'; import { TariffQueryString } from './queries/Tariff'; @@ -128,10 +129,10 @@ export interface ISubscriptionRepository extends CrudRepository { deleteByKey(key: string): Promise; } -export interface ITransactionEventRepository extends CrudRepository { +export interface ITransactionEventRepository extends CrudRepository { createOrUpdateTransactionByTransactionEventAndStationId(value: OCPP2_0_1.TransactionEventRequest, stationId: string): Promise; createMeterValue(value: OCPP2_0_1.MeterValueType, transactionDatabaseId?: number | null): Promise; - readAllByStationIdAndTransactionId(stationId: string, transactionId: string): Promise; + readAllByStationIdAndTransactionId(stationId: string, transactionId: string): Promise; readTransactionByStationIdAndTransactionId(stationId: string, transactionId: string): Promise; readAllTransactionsByStationIdAndEvseAndChargingStates(stationId: string, evse: OCPP2_0_1.EVSEType, chargingStates?: OCPP2_0_1.ChargingStateEnumType[]): Promise; readAllActiveTransactionsByIdToken(idToken: OCPP2_0_1.IdTokenType): Promise; diff --git a/01_Data/src/layers/sequelize/index.ts b/01_Data/src/layers/sequelize/index.ts index cd78b679..c382e1b2 100644 --- a/01_Data/src/layers/sequelize/index.ts +++ b/01_Data/src/layers/sequelize/index.ts @@ -43,3 +43,6 @@ export { SequelizeChargingStationSequenceRepository } from './repository/Chargin // Sequelize Utilities export { DefaultSequelizeInstance } from './util'; + +// Sequelize Mappers +export * as OCPP2_0_1_Mapper from './mapper/2.0.1'; \ No newline at end of file diff --git a/01_Data/src/layers/sequelize/mapper/2.0.1/MeterValueMapper.ts b/01_Data/src/layers/sequelize/mapper/2.0.1/MeterValueMapper.ts new file mode 100644 index 00000000..f8d7c9a8 --- /dev/null +++ b/01_Data/src/layers/sequelize/mapper/2.0.1/MeterValueMapper.ts @@ -0,0 +1,47 @@ +// Copyright Contributors to the CitrineOS Project +// +// SPDX-License-Identifier: Apache 2.0 + +import { OCPP2_0_1 } from '@citrineos/base'; +import { MeterValue } from '../../model/TransactionEvent'; + +export class MeterValueMapper { + static toMeterValueType(meterValue: MeterValue): OCPP2_0_1.MeterValueType { + return { + timestamp: meterValue.timestamp, + sampledValue: MeterValueMapper.toSampledValueTypes(meterValue.sampledValue), + customData: meterValue.customData, + } + } + + static toSampledValueTypes(sampledValues: any): [OCPP2_0_1.SampledValueType, ...OCPP2_0_1.SampledValueType[]] { + if (!(sampledValues instanceof Array) || sampledValues.length === 0) { + throw new Error(`Invalid sampledValues: ${JSON.stringify(sampledValues)}`); + } + + const sampledValueTypes: OCPP2_0_1.SampledValueType[] = []; + for (const sampledValue of sampledValues) { + sampledValueTypes.push({ + value: sampledValue.value, + context: sampledValue.context, + measurand: sampledValue.measurand, + phase: sampledValue.phase, + location: sampledValue.location, + signedMeterValue: sampledValue.signedMeterValue ? { + signedMeterData: sampledValue.signedMeterValue.signedMeterData, + signingMethod: sampledValue.signedMeterValue.signingMethod, + encodingMethod: sampledValue.signedMeterValue.encodingMethod, + publicKey: sampledValue.signedMeterValue.publicKey, + customData: sampledValue.signedMeterValue.customData, + } : undefined, + unitOfMeasure: sampledValue.unitOfMeasure ? { + unit: sampledValue.unitOfMeasure.unit, + multiplier: sampledValue.unitOfMeasure.multiplier, + customData: sampledValue.unitOfMeasure.customData, + }: undefined, + customData: sampledValue.customData, + }); + } + return sampledValueTypes as [OCPP2_0_1.SampledValueType, ...OCPP2_0_1.SampledValueType[]]; + } +} \ No newline at end of file diff --git a/01_Data/src/layers/sequelize/mapper/2.0.1/index.ts b/01_Data/src/layers/sequelize/mapper/2.0.1/index.ts new file mode 100644 index 00000000..9ea73b0a --- /dev/null +++ b/01_Data/src/layers/sequelize/mapper/2.0.1/index.ts @@ -0,0 +1,5 @@ +// Copyright Contributors to the CitrineOS Project +// +// SPDX-License-Identifier: Apache 2.0 + +export { MeterValueMapper } from './MeterValueMapper'; \ No newline at end of file diff --git a/01_Data/src/layers/sequelize/model/TransactionEvent/MeterValue.ts b/01_Data/src/layers/sequelize/model/TransactionEvent/MeterValue.ts index 1afcb11d..b8003f6a 100644 --- a/01_Data/src/layers/sequelize/model/TransactionEvent/MeterValue.ts +++ b/01_Data/src/layers/sequelize/model/TransactionEvent/MeterValue.ts @@ -3,14 +3,14 @@ // // SPDX-License-Identifier: Apache 2.0 -import { OCPP2_0_1_Namespace, OCPP2_0_1 } from '@citrineos/base'; +import { Namespace } from '@citrineos/base'; import { Column, DataType, ForeignKey, Model, Table } from 'sequelize-typescript'; import { TransactionEvent } from './TransactionEvent'; import { Transaction } from './Transaction'; @Table -export class MeterValue extends Model implements OCPP2_0_1.MeterValueType { - static readonly MODEL_NAME: string = OCPP2_0_1_Namespace.MeterValueType; +export class MeterValue extends Model { + static readonly MODEL_NAME: string = Namespace.MeterValue; @ForeignKey(() => TransactionEvent) @Column(DataType.INTEGER) @@ -21,7 +21,7 @@ export class MeterValue extends Model implements OCPP2_0_1.MeterValueType { declare transactionDatabaseId?: number | null; @Column(DataType.JSON) - declare sampledValue: [OCPP2_0_1.SampledValueType, ...OCPP2_0_1.SampledValueType[]]; + declare sampledValue: [object, ...object[]]; @Column({ type: DataType.DATE, @@ -31,5 +31,5 @@ export class MeterValue extends Model implements OCPP2_0_1.MeterValueType { }) declare timestamp: string; - declare customData?: OCPP2_0_1.CustomDataType | null; + declare customData?: any | null; } diff --git a/01_Data/src/layers/sequelize/model/TransactionEvent/TransactionEvent.ts b/01_Data/src/layers/sequelize/model/TransactionEvent/TransactionEvent.ts index fdab149e..4d7c8bfa 100644 --- a/01_Data/src/layers/sequelize/model/TransactionEvent/TransactionEvent.ts +++ b/01_Data/src/layers/sequelize/model/TransactionEvent/TransactionEvent.ts @@ -11,7 +11,7 @@ import { MeterValue } from './MeterValue'; import { Transaction } from './Transaction'; @Table -export class TransactionEvent extends Model implements OCPP2_0_1.TransactionEventRequest { +export class TransactionEvent extends Model { static readonly MODEL_NAME: string = OCPP2_0_1_Namespace.TransactionEventRequest; @Column diff --git a/01_Data/src/layers/sequelize/repository/TransactionEvent.ts b/01_Data/src/layers/sequelize/repository/TransactionEvent.ts index fceb8e06..f6475793 100644 --- a/01_Data/src/layers/sequelize/repository/TransactionEvent.ts +++ b/01_Data/src/layers/sequelize/repository/TransactionEvent.ts @@ -12,6 +12,7 @@ import { Evse } from '../model/DeviceModel'; import { Op, WhereOptions } from 'sequelize'; import { Sequelize } from 'sequelize-typescript'; import { ILogObj, Logger } from 'tslog'; +import { MeterValueMapper } from '../mapper/2.0.1'; export class SequelizeTransactionEventRepository extends SequelizeRepository implements ITransactionEventRepository { transaction: CrudRepository; @@ -142,8 +143,10 @@ export class SequelizeTransactionEventRepository extends SequelizeRepository MeterValueMapper.toMeterValueType(meterValue) + ); + await finalTransaction.update({ totalKwh: MeterValueUtils.getTotalKwh(meterValueTypes) }, { transaction: sequelizeTransaction }); await finalTransaction.reload({ include: [{ model: TransactionEvent, as: Transaction.TRANSACTION_EVENTS_ALIAS, include: [IdToken] }, MeterValue, Evse], transaction: sequelizeTransaction, @@ -155,7 +158,7 @@ export class SequelizeTransactionEventRepository extends SequelizeRepository { + async readAllByStationIdAndTransactionId(stationId: string, transactionId: string): Promise { return await super .readAllByQuery({ where: { stationId }, diff --git a/01_Data/test/layers/sequelize/mapper/2.0.1/MeterValueMapper.test.ts b/01_Data/test/layers/sequelize/mapper/2.0.1/MeterValueMapper.test.ts new file mode 100644 index 00000000..489bd3a7 --- /dev/null +++ b/01_Data/test/layers/sequelize/mapper/2.0.1/MeterValueMapper.test.ts @@ -0,0 +1,17 @@ +import { expect } from '@jest/globals'; +import { MeterValueMapper } from '../../../../../src/layers/sequelize/mapper/2.0.1'; +import { aMeterValue } from '../../../../providers/MeterValue'; + +describe('MeterValueMapper', () => { + describe('map MeterValue and MeterValueMapper', () => { + it('should map between MeterValue and MeterValueMapper successfully', () => { + const givenMeterValue = aMeterValue(); + + const actualMapper = MeterValueMapper.toMeterValueType(givenMeterValue); + expect(actualMapper).toBeTruthy(); + expect(actualMapper.timestamp).toBe(givenMeterValue.timestamp); + expect(actualMapper.sampledValue).toEqual(givenMeterValue.sampledValue); + expect(actualMapper.customData).toEqual(givenMeterValue.customData); + }); + }); +}); diff --git a/01_Data/test/providers/MeterValue.ts b/01_Data/test/providers/MeterValue.ts new file mode 100644 index 00000000..e47a4f8d --- /dev/null +++ b/01_Data/test/providers/MeterValue.ts @@ -0,0 +1,46 @@ +import { applyUpdateFunction, UpdateFunction } from '../utils/UpdateUtil'; +import { faker } from '@faker-js/faker'; +import { MeterValue } from '../../src'; + +export function aMeterValue(updateFunction?: UpdateFunction): MeterValue { + const meterValue: MeterValue = { + sampledValue: [...[aOcpp201SampledValue()]], + timestamp: faker.date.recent().toISOString(), + customData: { + vendorId: faker.string.alphanumeric(5), + } + } as MeterValue; + + return applyUpdateFunction(meterValue, updateFunction); +} + +export function aOcpp201SampledValue(updateFunction?: UpdateFunction): object { + const sampledValue: object = { + measurand: 'Energy.Active.Import.Register', + phase: 'L1', + unitOfMeasure: { + unit: 'kWh', + multiplier: faker.number.int({ min: 0, max: 100 }), + customData: { + vendorId: faker.string.alphanumeric(5), + } + }, + value: faker.number.int({ min: 0, max: 100 }), + context: 'Transaction.Begin', + location: 'Outlet', + signedMeterValue: { + signedMeterData: faker.string.alphanumeric(5), + signingMethod: faker.string.alphanumeric(5), + encodingMethod: faker.string.alphanumeric(5), + publicKey: faker.string.alphanumeric(5), + customData: { + vendorId: faker.string.alphanumeric(5), + } + }, + customData: { + vendorId: faker.string.alphanumeric(5), + } + } as object; + + return applyUpdateFunction(sampledValue, updateFunction); +} diff --git a/01_Data/test/utils/UpdateUtil.ts b/01_Data/test/utils/UpdateUtil.ts new file mode 100644 index 00000000..e0c8c121 --- /dev/null +++ b/01_Data/test/utils/UpdateUtil.ts @@ -0,0 +1,8 @@ +export type UpdateFunction = (item: T) => void; + +export const applyUpdateFunction = (item: T, updateFunction?: UpdateFunction): T => { + if (updateFunction) { + updateFunction(item); + } + return item; +}; \ No newline at end of file diff --git a/03_Modules/Transactions/src/module/TransactionService.ts b/03_Modules/Transactions/src/module/TransactionService.ts index b1bd399c..078c2494 100644 --- a/03_Modules/Transactions/src/module/TransactionService.ts +++ b/03_Modules/Transactions/src/module/TransactionService.ts @@ -3,6 +3,7 @@ import { IAuthorizationRepository, ITransactionEventRepository, Transaction, + OCPP2_0_1_Mapper, } from '@citrineos/data'; import { IMessageContext, @@ -33,11 +34,13 @@ export class TransactionService { } async recalculateTotalKwh(transactionDbId: number) { - const totalKwh = MeterValueUtils.getTotalKwh( - await this._transactionEventRepository.readAllMeterValuesByTransactionDataBaseId( - transactionDbId, - ), + const meterValues = await this._transactionEventRepository.readAllMeterValuesByTransactionDataBaseId( + transactionDbId, ); + const meterValueTypes = meterValues.map( + meterValue => OCPP2_0_1_Mapper.MeterValueMapper.toMeterValueType(meterValue) + ); + const totalKwh = MeterValueUtils.getTotalKwh(meterValueTypes); await Transaction.update( { totalKwh: totalKwh },