import { ConfigurationService } from '@energyweb/origin-backend' ; import { DeviceCreateData , DeviceStatus , IDevice , IDeviceProductInfo , IEnergyGeneratedWithStatus , IExternalDeviceId , ILoggedInUser , ISmartMeterRead , ISmartMeterReadingsAdapter , ISmartMeterReadStats , ISmartMeterReadWithStatus , ISuccessResponse , ResponseFailure , sortLowestToHighestTimestamp } from '@energyweb/origin-backend-core' ; import { StorageErrors } from '@energyweb/origin-backend-utils' ; import { Inject , Injectable , NotFoundException , UnprocessableEntityException } from '@nestjs/common' ; import { EventBus } from '@nestjs/cqrs' ; import { InjectRepository } from '@nestjs/typeorm' ; import { validate } from 'class-validator' ; import { BigNumber } from 'ethers' ; import { FindOneOptions , Repository } from 'typeorm' ; import { v4 as uuid } from 'uuid' ; import { Device } from './device.entity' ; import { DeviceStatusChangedEvent } from './events' ; import { DeviceCreatedEvent } from './events/device-created.event' ; export const SM_READS_ADAPTER = 'SM_READS_ADAPTER' ; @ Injectable ( ) export class DeviceService { constructor ( @ InjectRepository ( Device ) private readonly repository , private readonly configurationService , private readonly eventBus , @ Inject ( SM_READS_ADAPTER ) private smartMeterReadingsAdapter ? ) { } async findByExternalId ( externalId ) { const devices = ( await this . repository . find ( ) ) as IDevice [ ] ; const device = devices . find ( ( d ) => d . externalDeviceIds . find ( ( id ) => id . id === externalId . id && id . type === externalId . type ) ) ; return device ; } async findOne ( id , withMeterStats = false ) { const device = ( await this . repository . findOne ( id ) ) as IDevice ; if ( this . smartMeterReadingsAdapter ) { device . smartMeterReads = [ ] ; } if ( withMeterStats ) { device . meterStats = await this . getMeterStats ( device . id . toString ( ) ) ; } return device ; } async create ( data , loggedUser ) { const configuration = await this . configurationService . get ( ) ; const newEntity = new Device ( ) ; Object . assign ( newEntity , { ... data , status : data . status ?? DeviceStatus . Submitted , smartMeterReads : data . smartMeterReads ?? [ ] , deviceGroup : data . deviceGroup ?? '' , organizationId : loggedUser . organizationId , externalDeviceIds : data . externalDeviceIds ? data . externalDeviceIds . map ( ( { id , type } ) => { if ( typeof id === 'undefined' && configuration . externalDeviceIdTypes ?. find ( ( t ) => t . type === type ) ?. autogenerated ) { return { id : uuid ( ) , type } ; } return { id , type } ; } ) : [ ] } ) ; const validationErrors = await validate ( newEntity ) ; if ( validationErrors . length > 0 ) { throw new UnprocessableEntityException ( { success : false , errors : validationErrors } ) ; } await this . repository . save ( newEntity ) ; const device = await this . findOne ( newEntity . id . toString ( ) ) ; this . eventBus . publish ( new DeviceCreatedEvent ( device , loggedUser . id ) ) ; return device ; } async remove ( entity ) { await this . repository . remove ( entity ) ; } async getAllSmartMeterReadings ( id ) { const device = await this . repository . findOne ( id ) ; if ( this . smartMeterReadingsAdapter ) { return this . smartMeterReadingsAdapter . getAll ( device ) ; } return device . smartMeterReads ; } async addSmartMeterReadings ( id , newSmartMeterReads ) { const device = await this . findOne ( id ) ; if ( this . smartMeterReadingsAdapter ) { try { await this . smartMeterReadingsAdapter . save ( device , newSmartMeterReads ) ; } catch ( error ) { throw new UnprocessableEntityException ( { success : false , message : error . message } ) ; } return { success : true , message : `Smart meter readings successfully added to device ${ id }` } ; } if ( device . smartMeterReads . length > 0 ) { newSmartMeterReads . forEach ( ( newSmartMeterRead ) => { if ( newSmartMeterRead . timestamp <= device . smartMeterReads [ device . smartMeterReads . length - 1 ] . timestamp ) { throw new UnprocessableEntityException ( { success : false , message : `Smart meter readings timestamp should always be higher than latest.` } ) ; } } ) ; } await this . repository . update ( device . id , { smartMeterReads : [ ... device . smartMeterReads , ... newSmartMeterReads ] . sort ( sortLowestToHighestTimestamp ) } ) ; return { success : true , message : `Smart meter readings successfully added to device ${ id }` } ; } async getAll ( withMeterStats = false , options = { } ) { const devices = ( await this . repository . find ( { ... options } ) ) as IDevice [ ] ; return withMeterStats ? this . attachMeterStats ( devices ) : devices ; } async getOrganizationDevices ( organizationId , withMeterStats = false ) { const devices = ( await this . repository . find ( { where : { organizationId } } ) ) as IDevice [ ] ; return withMeterStats ? this . attachMeterStats ( devices ) : devices ; } async findDeviceProductInfo ( externalId ) { const devices = await this . repository . find ( ) ; return devices . find ( ( device ) => device . externalDeviceIds . find ( ( id ) => id . id === externalId . id && id . type === externalId . type ) ) ; } async updateStatus ( id , status ) { const device = await this . findOne ( id ) ; if ( ! device ) { throw new NotFoundException ( ResponseFailure ( StorageErrors . NON_EXISTENT ) ) ; } await this . repository . update ( device . id , { status } ) ; this . eventBus . publish ( new DeviceStatusChangedEvent ( device , status ) ) ; return this . findOne ( id ) ; } private async getMeterStats ( deviceId ) { const smReads = await this . getAllSmartMeterReadings ( deviceId ) ; return this . calculateCertifiedEnergy ( smReads . map ( ( smRead ) => ( { ... smRead , certified : false } ) ) ) ; } private calculateCertifiedEnergy ( smReads ) { const energiesGenerated = [ ] ; for ( let i = 0 ; i < smReads . length ; i ++ ) { const isFirstReading = i === 0 ; const { meterReading , timestamp , certified } = smReads [ i ] ; energiesGenerated . push ( { energy : BigNumber . from ( meterReading ) . sub ( isFirstReading ? 0 : BigNumber . from ( smReads [ i - 1 ] . meterReading ) ) , timestamp , certified } ) ; } const sumEnergy = ( energyGens ) => energyGens . reduce ( ( sum , energyGen ) => sum . add ( energyGen . energy ) , BigNumber . from ( 0 ) ) ; return { certified : sumEnergy ( energiesGenerated . filter ( ( energyGen ) => energyGen . certified ) ) , uncertified : sumEnergy ( energiesGenerated . filter ( ( energyGen ) => ! energyGen . certified ) ) } ; } private async attachMeterStats ( devices ) { return Promise . all ( devices . map ( async ( d ) => { const device = d ; if ( this . smartMeterReadingsAdapter ) { device . smartMeterReads = [ ] ; } device . meterStats = await this . getMeterStats ( device . id . toString ( ) ) ; return device ; } ) ) ; } }