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: Repository, private readonly configurationService: ConfigurationService, private readonly eventBus: EventBus, @Inject(SM_READS_ADAPTER) private smartMeterReadingsAdapter?: ISmartMeterReadingsAdapter ) {} async findByExternalId(externalId: IExternalDeviceId): Promise { 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: string, withMeterStats = false): Promise { 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: DeviceCreateData, loggedUser: ILoggedInUser): Promise { 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: Device): Promise { await this.repository.remove(entity); } async getAllSmartMeterReadings(id: string): Promise { const device = await this.repository.findOne(id); if (this.smartMeterReadingsAdapter) { return this.smartMeterReadingsAdapter.getAll(device); } return device.smartMeterReads; } async addSmartMeterReadings( id: string, newSmartMeterReads: ISmartMeterRead[] ): Promise { 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: FindOneOptions = {}): Promise { const devices = (await this.repository.find({ ...options })) as IDevice[]; return withMeterStats ? this.attachMeterStats(devices) : devices; } async getOrganizationDevices( organizationId: number, withMeterStats = false ): Promise { const devices = (await this.repository.find({ where: { organizationId } })) as IDevice[]; return withMeterStats ? this.attachMeterStats(devices) : devices; } async findDeviceProductInfo(externalId: IExternalDeviceId): Promise { 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: string, status: DeviceStatus): Promise { 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: string): Promise { const smReads = await this.getAllSmartMeterReadings(deviceId); return this.calculateCertifiedEnergy( smReads.map((smRead) => ({ ...smRead, certified: false })) ); } private calculateCertifiedEnergy(smReads: ISmartMeterReadWithStatus[]): ISmartMeterReadStats { const energiesGenerated: IEnergyGeneratedWithStatus[] = []; 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: IEnergyGeneratedWithStatus[]) => 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: IDevice[]) { 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; }) ); } }